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- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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:
|
| 1353 |
-
|
| 1354 |
-
|
|
|
|
| 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 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = []
|