extracting-lab-tables
GitHub从PDF、扫描件或图片中检测并提取实验室面板表格,通过OCR解析和PHI去标识化,输出包含测试名称、值、单位及异常标志的结构化行数据,供后续LOINC映射和FHIR转换使用。
Trigger Scenarios
Install
npx skills add maziyarpanahi/openmed --skill extracting-lab-tables -g -y
SKILL.md
Frontmatter
{
"name": "extracting-lab-tables",
"license": "Apache-2.0",
"metadata": {
"pairs": "before",
"project": "OpenMed",
"version": "1.0",
"category": "imaging-ocr"
},
"description": "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."
}
Extracting lab tables from documents and scans
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).
When to use
- You have a lab report as a scanned image / photo / PDF page and need the panel as rows, not pixels.
- The source is a CSV/TSV export and you need columns classified (which is the value, the unit, the range, the flag) and PHI columns redacted.
- You need machine-readable rows to feed LOINC mapping and a FHIR
Observation/DiagnosticReport.
What OpenMed gives you here
openmed.multimodal ships the intake primitives (no heavy deps at import; the
OCR backend loads lazily):
openmed.multimodal.ocr.ocr(image, engine=...)→ anOcrResultwhose.wordsareOcrWord(text, bbox, confidence, page)and.textis the joined string.OcrResult.to_document()bridges each word (with its pixel bbox) into anExtractedDocumentso detected PHI can project back to the source location.read_table(...)→ aTableView(headers,rows,delimiter,has_header,columns) for delimited text;classify_columns(...)labels each column;redact_table(...)→ aRedactedTablewith a PHI-safemanifest.
Engines: Tesseract (pip install "openmed[multimodal]" + the system binary) or
PaddleOCR (pip install "openmed[ocr-paddle]"). ocr() auto-selects the first
installed backend.
Quick start
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.
Workflow
- Detect the source type. CSV/TSV →
read_table. Image/scan →ocr(). PDF/DOCX are not directly parseable (they raiseUnsupportedDocumentError); render PDF pages to images first, or extract their text layer, then OCR. - OCR with positions.
ocr()returnsOcrWords carryingbboxandpage. Keep the boxes — they let you cluster words into rows/columns and project PHI redaction back to pixels. - Reconstruct the grid. Cluster words by their
bboxy 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. - Identify the lab columns. Map headers to roles: test name, value,
unit, reference range, flag. For delimited input,
classify_columnstags PHI columns (name/MRN/DOB) soredact_tablemasks them. - De-identify embedded PHI. Patient name/MRN often sit in the table header or
a leading column. Redact those columns (
redact_table) and run free-text cells throughopenmed.deidentifybefore the rows leave the device. - Emit structured rows
{test, value, unit, ref_range, flag}per result and hand off to LOINC/UCUM coding andparsing-lab-values.
Hand-off to / from OpenMed
- To
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. - To
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. - To FHIR (
exporting-to-fhir): each row becomes anObservation(code=LOINC,valueQuantitywith UCUMunit,referenceRange,interpretation) grouped under aDiagnosticReport. - De-identify with
deidentifying-clinical-text(openmed.deidentify) before export. OCR words carry pixel boxes so redaction maps back to the image. - Everything here runs on-device; no scan or row leaves the process un-de-identified.
Edge cases & gotchas
- PDF/DOCX raise
UnsupportedDocumentError. The multimodal dispatcher has no PDF/DOCX handler — rasterize PDF pages to PNG (or pull the text layer) before callingocr(). Image formats (PNG/JPG/TIFF/…) and CSV/TSV are handled. - OCR returns words, not a table. You must reconstruct rows/columns from
bboxgeometry. Multi-line cells, wrapped test names, and merged header cells break naive x/y bucketing — tune the clustering tolerance per template. - Reference ranges are easy to mis-split. "12.0-15.5", "<5", "70 - 99", and
en/em dashes must survive OCR and tokenization as one cell. Don't let a space
or a misread dash fracture the range —
parse_reference_rangedownstream expects it whole. - Units belong to the value, not the range. Keep "9.1 g/dL" and the range "12.0-15.5" in separate fields; the value's unit must match the range's unit or the abnormal flag will be wrong (the flag helper is unit-agnostic).
- Low-confidence cells. Gate on
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. - PHI hides in tables. Patient name, MRN, DOB, and accession numbers commonly occupy the header or first column. Classify and redact them; never log the raw table.
- Engine availability.
ocr()raises a clearMissingDependencyErrorif no backend is installed — install Tesseract or PaddleOCR per the extras.
Standards & references
- LOINC — universal lab observation codes: https://loinc.org/
- UCUM — Unified Code for Units of Measure: https://ucum.org/
- HL7 FHIR R4 Observation (
valueQuantity,referenceRange,interpretation): https://hl7.org/fhir/R4/observation.html - HL7 FHIR R4 DiagnosticReport (lab grouping): https://hl7.org/fhir/R4/diagnosticreport.html
- Tesseract OCR: https://github.com/tesseract-ocr/tesseract
- PaddleOCR: https://github.com/PaddlePaddle/PaddleOCR
- OpenMed source:
openmed/multimodal/ocr.py,openmed/multimodal/tabular_csv.py.
Version History
- f213557 Current 2026-07-23 00:44


