Commit ·
80dc111
1
Parent(s): 0c16597
Fix bounding box grounding coordinate detection, crop OCR line parser, and render all extracted medicines in UI
Browse files- gradio_pharmacopilot_demo.py +84 -16
gradio_pharmacopilot_demo.py
CHANGED
|
@@ -613,12 +613,29 @@ def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str,
|
|
| 613 |
search_source = focused_section if focused_section else ocr_text
|
| 614 |
|
| 615 |
drugs = []
|
| 616 |
-
# Find all numbered lines like "1. Tab. Napa" or "- Cap. Pregab" or "1) Tab. Diclo"
|
| 617 |
for line in search_source.split("\n"):
|
| 618 |
line = line.strip()
|
| 619 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
drugs.append(line)
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
# Parse each matched drug line
|
| 623 |
for d in drugs:
|
| 624 |
parsed_med = parse_drug_line(d)
|
|
@@ -1082,12 +1099,6 @@ def extraction_card_html(extraction: dict[str, Any]) -> str:
|
|
| 1082 |
("Address", "address"), ("DEA Number", "dea_number"),
|
| 1083 |
("NPI Number", "npi_number"), ("Phone", "phone_number"),
|
| 1084 |
]),
|
| 1085 |
-
("Prescription Details", "prescription_details", [
|
| 1086 |
-
("Date Issued", "date_of_issuance"), ("Drug Name", "drug_name"),
|
| 1087 |
-
("Strength", "strength"), ("Dosage Form", "dosage_form"),
|
| 1088 |
-
("Quantity", "quantity"), ("Directions (Sig)", "directions_sig"),
|
| 1089 |
-
("Refills", "refills_authorized"), ("Dispense As Written", "dispense_as_written"),
|
| 1090 |
-
]),
|
| 1091 |
]
|
| 1092 |
|
| 1093 |
legibility = extraction.get("document_metadata", {}).get("overall_legibility_score", 0)
|
|
@@ -1109,6 +1120,55 @@ def extraction_card_html(extraction: dict[str, Any]) -> str:
|
|
| 1109 |
)
|
| 1110 |
html_parts.append('</dl></div>')
|
| 1111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
html_parts.append('</div>')
|
| 1113 |
return "\n".join(html_parts)
|
| 1114 |
|
|
@@ -1290,14 +1350,21 @@ def run_minicpm_ocr(pil_image: Image.Image) -> tuple[str, Image.Image]:
|
|
| 1290 |
OCR_MODEL = OCR_MODEL.cuda()
|
| 1291 |
|
| 1292 |
# Pass 1A: Grounding to detect handwritten prescription items
|
| 1293 |
-
grounding_prompt = "Identify
|
| 1294 |
grounding_output = _run_minicpm_single_pass(pil_image, grounding_prompt, max_tokens=512)
|
| 1295 |
|
| 1296 |
-
# Parse coordinates
|
|
|
|
|
|
|
| 1297 |
boxes = []
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1301 |
|
| 1302 |
width, height = pil_image.size
|
| 1303 |
cropped_ocr_results = []
|
|
@@ -1313,8 +1380,9 @@ def run_minicpm_ocr(pil_image: Image.Image) -> tuple[str, Image.Image]:
|
|
| 1313 |
xmax = int(xmax_n * width / 1000)
|
| 1314 |
|
| 1315 |
# Draw bounding box
|
| 1316 |
-
draw.rectangle([xmin, ymin, xmax, ymax], outline="#
|
| 1317 |
-
draw.
|
|
|
|
| 1318 |
|
| 1319 |
# Crop region with 15px padding
|
| 1320 |
padding = 15
|
|
|
|
| 613 |
search_source = focused_section if focused_section else ocr_text
|
| 614 |
|
| 615 |
drugs = []
|
|
|
|
| 616 |
for line in search_source.split("\n"):
|
| 617 |
line = line.strip()
|
| 618 |
+
if not line:
|
| 619 |
+
continue
|
| 620 |
+
if focused_section:
|
| 621 |
+
if "drug extraction" in line.lower() or "=== " in line:
|
| 622 |
+
continue
|
| 623 |
drugs.append(line)
|
| 624 |
+
else:
|
| 625 |
+
if re.search(r'\b(tab\.|cap\.|syp\.|inj\.|tablet|capsule|syrup|medicine|rx)\b', line, re.I) or re.match(r'^[\d\-]+[\.\)]?\s+', line):
|
| 626 |
+
drugs.append(line)
|
| 627 |
+
|
| 628 |
+
# Deduplicate extracted drug lines while preserving order
|
| 629 |
+
seen_drugs = set()
|
| 630 |
+
unique_drugs = []
|
| 631 |
+
for d in drugs:
|
| 632 |
+
d_clean = d.strip()
|
| 633 |
+
norm_d = normalize(d_clean)
|
| 634 |
+
if norm_d not in seen_drugs and d_clean:
|
| 635 |
+
seen_drugs.add(norm_d)
|
| 636 |
+
unique_drugs.append(d_clean)
|
| 637 |
+
drugs = unique_drugs
|
| 638 |
+
|
| 639 |
# Parse each matched drug line
|
| 640 |
for d in drugs:
|
| 641 |
parsed_med = parse_drug_line(d)
|
|
|
|
| 1099 |
("Address", "address"), ("DEA Number", "dea_number"),
|
| 1100 |
("NPI Number", "npi_number"), ("Phone", "phone_number"),
|
| 1101 |
]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1102 |
]
|
| 1103 |
|
| 1104 |
legibility = extraction.get("document_metadata", {}).get("overall_legibility_score", 0)
|
|
|
|
| 1120 |
)
|
| 1121 |
html_parts.append('</dl></div>')
|
| 1122 |
|
| 1123 |
+
# Add the Medications list section
|
| 1124 |
+
html_parts.append(f'<div class="extraction-section">')
|
| 1125 |
+
html_parts.append(f'<h4>All Extracted Medications</h4>')
|
| 1126 |
+
meds = extraction.get("medications", [])
|
| 1127 |
+
if meds:
|
| 1128 |
+
html_parts.append('<table class="candidate-table" style="width: 100%; border-collapse: collapse; margin-top: 8px;">')
|
| 1129 |
+
html_parts.append('<thead><tr><th>#</th><th>Drug Name</th><th>Dosage Form</th><th>Strength</th><th>Directions (Sig)</th></tr></thead>')
|
| 1130 |
+
html_parts.append('<tbody>')
|
| 1131 |
+
for idx, med in enumerate(meds, start=1):
|
| 1132 |
+
dname = med.get("drug_name", {}).get("value") or "Unknown"
|
| 1133 |
+
dname_conf = med.get("drug_name", {}).get("confidence", 0.0)
|
| 1134 |
+
|
| 1135 |
+
form = med.get("dosage_form", {}).get("value") or "-"
|
| 1136 |
+
strength = med.get("strength", {}).get("value") or "-"
|
| 1137 |
+
sig = med.get("directions_sig", {}).get("value") or "-"
|
| 1138 |
+
|
| 1139 |
+
html_parts.append(
|
| 1140 |
+
f'<tr>'
|
| 1141 |
+
f'<td>{idx}</td>'
|
| 1142 |
+
f'<td><strong>{dname}</strong> {_confidence_badge(dname_conf)}</td>'
|
| 1143 |
+
f'<td>{form}</td>'
|
| 1144 |
+
f'<td>{strength}</td>'
|
| 1145 |
+
f'<td><code>{sig}</code></td>'
|
| 1146 |
+
f'</tr>'
|
| 1147 |
+
)
|
| 1148 |
+
html_parts.append('</tbody></table>')
|
| 1149 |
+
else:
|
| 1150 |
+
html_parts.append('<p style="color: var(--muted); font-style: italic;">No medications detected.</p>')
|
| 1151 |
+
html_parts.append('</div>')
|
| 1152 |
+
|
| 1153 |
+
# Add Prescription Details (refills, date issued, etc.)
|
| 1154 |
+
html_parts.append(f'<div class="extraction-section">')
|
| 1155 |
+
html_parts.append(f'<h4>Prescription Metadata</h4>')
|
| 1156 |
+
html_parts.append('<dl class="extraction-fields">')
|
| 1157 |
+
meta_fields = [
|
| 1158 |
+
("Date Issued", "date_of_issuance"),
|
| 1159 |
+
("Refills Authorized", "refills_authorized"),
|
| 1160 |
+
("Dispense As Written", "dispense_as_written"),
|
| 1161 |
+
]
|
| 1162 |
+
for label, field_key in meta_fields:
|
| 1163 |
+
field = extraction.get("prescription_details", {}).get(field_key, {})
|
| 1164 |
+
val = field.get("value")
|
| 1165 |
+
conf = field.get("confidence", 0.0)
|
| 1166 |
+
html_parts.append(
|
| 1167 |
+
f'<dt>{label}</dt>'
|
| 1168 |
+
f'<dd>{_display_value(val)} {_confidence_badge(conf)}</dd>'
|
| 1169 |
+
)
|
| 1170 |
+
html_parts.append('</dl></div>')
|
| 1171 |
+
|
| 1172 |
html_parts.append('</div>')
|
| 1173 |
return "\n".join(html_parts)
|
| 1174 |
|
|
|
|
| 1350 |
OCR_MODEL = OCR_MODEL.cuda()
|
| 1351 |
|
| 1352 |
# Pass 1A: Grounding to detect handwritten prescription items
|
| 1353 |
+
grounding_prompt = "Identify all handwritten text regions (such as patient name, patient age, prescriber signature, drug name, dosage, refills). Return the coordinate boxes of these regions in [[ymin,xmin,ymax,xmax]] format."
|
| 1354 |
grounding_output = _run_minicpm_single_pass(pil_image, grounding_prompt, max_tokens=512)
|
| 1355 |
|
| 1356 |
+
# Parse coordinates robustly supporting both brackets [[ymin,xmin,ymax,xmax]] and parentheses (ymin,xmin,ymax,xmax)
|
| 1357 |
+
normalized_output = re.sub(r'\s+', ' ', grounding_output)
|
| 1358 |
+
raw_matches = re.findall(r'(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})', normalized_output)
|
| 1359 |
boxes = []
|
| 1360 |
+
for m in raw_matches:
|
| 1361 |
+
try:
|
| 1362 |
+
ymin, xmin, ymax, xmax = map(int, m)
|
| 1363 |
+
if all(0 <= val <= 1000 for val in (ymin, xmin, ymax, xmax)):
|
| 1364 |
+
if ymax > ymin and xmax > xmin:
|
| 1365 |
+
boxes.append((ymin, xmin, ymax, xmax))
|
| 1366 |
+
except ValueError:
|
| 1367 |
+
continue
|
| 1368 |
|
| 1369 |
width, height = pil_image.size
|
| 1370 |
cropped_ocr_results = []
|
|
|
|
| 1380 |
xmax = int(xmax_n * width / 1000)
|
| 1381 |
|
| 1382 |
# Draw bounding box
|
| 1383 |
+
draw.rectangle([xmin, ymin, xmax, ymax], outline="#10b981", width=4)
|
| 1384 |
+
draw.rectangle([xmin, max(0, ymin - 20), xmin + 45, ymin], fill="#10b981")
|
| 1385 |
+
draw.text((xmin + 5, max(0, ymin - 18)), f"Rx {i}", fill="white")
|
| 1386 |
|
| 1387 |
# Crop region with 15px padding
|
| 1388 |
padding = 15
|