Mohibullah commited on
Commit
c72ce6b
·
1 Parent(s): 80dc111

Replace VLM grounding with OpenCV line layout analysis and filter non-drug crops using is_valid_drug_line

Browse files
Files changed (1) hide show
  1. gradio_pharmacopilot_demo.py +88 -17
gradio_pharmacopilot_demo.py CHANGED
@@ -579,7 +579,34 @@ def calculate_fallback_legibility(extraction: dict[str, Any]) -> float:
579
  scores.append(conf)
580
  if not scores:
581
  return 0.0
582
- return sum(scores) / len(scores)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
 
584
 
585
  def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str, Any]:
@@ -620,10 +647,14 @@ def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str,
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()
@@ -1349,22 +1380,62 @@ def run_minicpm_ocr(pil_image: Image.Image) -> tuple[str, Image.Image]:
1349
  if torch.cuda.is_available():
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 = []
 
579
  scores.append(conf)
580
  if not scores:
581
  return 0.0
582
+ def is_valid_drug_line(line: str) -> bool:
583
+ line_lower = line.lower()
584
+
585
+ # 1. Check if it contains standard drug forms
586
+ if re.search(r'\b(tab\.|cap\.|syp\.|inj\.|tablet|capsule|syrup|suspension|injection|cream|ointment|gel|drop|drops|spray|inhaler)\b', line_lower):
587
+ return True
588
+
589
+ # 2. Check if it matches a known brand or generic name in the database
590
+ cleaned = re.sub(r'^\d+[\.\)]?\s*', '', line_lower).strip()
591
+ words = cleaned.split()
592
+ if words:
593
+ first_word = words[0].strip(" ,.-+()[]{}")
594
+ if first_word in BD_BRAND_TO_GENERIC or normalize(first_word) in MED_BY_NAME:
595
+ return True
596
+ if len(words) > 1:
597
+ two_words = " ".join(words[:2]).strip(" ,.-+()[]{}")
598
+ if two_words in BD_BRAND_TO_GENERIC or normalize(two_words) in MED_BY_NAME:
599
+ return True
600
+
601
+ # 3. Check if it contains strength indicators or dosage patterns
602
+ if re.search(r'\b\d+\s*(mg|g|ml|mcg|%)\b', line_lower) or re.search(r'\b\d+[\+\-]\d+[\+\-]\d+\b', line_lower):
603
+ return True
604
+
605
+ # 4. Check if it contains common sig keywords
606
+ if re.search(r'\b(once daily|twice daily|daily|bid|tid|qid|qd|hs|po|cap|tab)\b', line_lower):
607
+ return True
608
+
609
+ return False
610
 
611
 
612
  def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str, Any]:
 
647
  if focused_section:
648
  if "drug extraction" in line.lower() or "=== " in line:
649
  continue
650
+ clean_line = re.sub(r'^\d+[\.\)]?\s*', '', line).strip()
651
+ if is_valid_drug_line(clean_line):
652
+ drugs.append(line)
653
  else:
654
  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):
655
+ clean_line = re.sub(r'^\d+[\.\)]?\s*', '', line).strip()
656
+ if is_valid_drug_line(clean_line):
657
+ drugs.append(line)
658
 
659
  # Deduplicate extracted drug lines while preserving order
660
  seen_drugs = set()
 
1380
  if torch.cuda.is_available():
1381
  OCR_MODEL = OCR_MODEL.cuda()
1382
 
1383
+ # Pass 1A: Detect text regions using OpenCV image processing (horizontal line-removal + contour extraction)
1384
+ # This acts as a robust engineering layout analysis (not just prompt engineering grounding)
1385
+ import numpy as np
1386
+ import cv2
1387
 
 
 
 
1388
  boxes = []
1389
+ try:
1390
+ # Convert PIL image to OpenCV grayscale
1391
+ img_np = np.array(pil_image.convert("RGB"))
1392
+ img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
1393
+ gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
1394
+
1395
+ # Otsu's binarization
1396
+ _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
1397
+
1398
+ h_img, w_img = gray.shape
1399
+
1400
+ # Detect and remove printed table/grid lines to isolate text
1401
+ h_size = max(15, int(w_img * 0.04))
1402
+ v_size = max(15, int(h_img * 0.04))
1403
+
1404
+ horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (h_size, 1))
1405
+ detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
1406
+
1407
+ vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, v_size))
1408
+ detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
1409
+
1410
+ clean = cv2.subtract(thresh, detect_horizontal)
1411
+ clean = cv2.subtract(clean, detect_vertical)
1412
+
1413
+ # Dilation to merge characters horizontally into cohesive text blocks
1414
+ d_w = max(5, int(w_img * 0.03))
1415
+ d_h = max(2, int(h_img * 0.005))
1416
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (d_w, d_h))
1417
+ dilated = cv2.dilate(clean, kernel, iterations=2)
1418
+
1419
+ # Find external contours
1420
+ contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
1421
+
1422
+ for c in contours:
1423
+ x, y, w, h = cv2.boundingRect(c)
1424
+ # Filter contours to target horizontal text lines
1425
+ if w > w_img * 0.04 and h > h_img * 0.01 and w < w_img * 0.95 and h < h_img * 0.2:
1426
+ if w > h * 1.1:
1427
+ # Convert to 0-1000 scale compatible with drawing/cropping code
1428
+ ymin_n = int(y / h_img * 1000)
1429
+ xmin_n = int(x / w_img * 1000)
1430
+ ymax_n = int((y + h) / h_img * 1000)
1431
+ xmax_n = int((x + w) / w_img * 1000)
1432
+ boxes.append((ymin_n, xmin_n, ymax_n, xmax_n))
1433
+
1434
+ # Sort boxes top-to-bottom
1435
+ boxes.sort(key=lambda b: b[0])
1436
+ except Exception as exc:
1437
+ print(f"OpenCV layout extraction error: {exc}")
1438
+ boxes = []
1439
 
1440
  width, height = pil_image.size
1441
  cropped_ocr_results = []