Mohibullah commited on
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
Files changed (1) hide show
  1. 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 re.search(r'\b(tab\.|cap\.|syp\.|inj\.)\b', line, re.I) or re.match(r'^\d+[\.\)]\s+', line):
 
 
 
 
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 each numbered handwritten medication line or direction. Return coordinate boxes in <box>(ymin,xmin,ymax,xmax)</box> format."
1294
  grounding_output = _run_minicpm_single_pass(pil_image, grounding_prompt, max_tokens=512)
1295
 
1296
- # Parse coordinates
 
 
1297
  boxes = []
1298
- matches = re.findall(r'\((\d+),(\d+),(\d+),(\d+)\)', grounding_output)
1299
- for m in matches:
1300
- boxes.append((int(m[0]), int(m[1]), int(m[2]), int(m[3])))
 
 
 
 
 
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="#0f9f6e", width=3)
1317
- draw.text((xmin + 5, max(0, ymin - 15)), f"Rx {i}", fill="#0f9f6e")
 
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