Ayaan Sharif
commited on
Commit
·
bf1a42b
1
Parent(s):
1ea1ae7
Fix critical issues and optimize resource utilization
Browse filesPerformance Optimizations:
- Set CPU thread counts (OMP, OPENBLAS, MKL) for multi-core usage
- Configure PyTorch to use 2 threads for YOLO inference
- Parallelize multi-scale signature detection with ThreadPoolExecutor
- Increase queue concurrency from 2 to 4 requests
Bug Fixes:
- Remove duplicate Legend markdown section (was appearing twice)
- Fix duplicate auto-process on file upload (now single combined call)
- Add NMS (Non-Maximum Suppression) to remove duplicate signature boxes
- Improve temp file handling (still needs cleanup, but better)
Code Quality:
- Combine preview and process into single file change event
- Better resource utilization for 2 CPU cores and 18GB RAM
- Reduce redundant processing calls
app.py
CHANGED
|
@@ -12,6 +12,14 @@ import io
|
|
| 12 |
import numpy as np
|
| 13 |
import cv2
|
| 14 |
from typing import List, Tuple, Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Optional imports for signature detection
|
| 17 |
try:
|
|
@@ -81,6 +89,14 @@ def load_signature_model() -> Optional["YOLO"]:
|
|
| 81 |
token=os.environ.get("HF_TOKEN")
|
| 82 |
)
|
| 83 |
_SIGNATURE_MODEL = YOLO(model_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return _SIGNATURE_MODEL
|
| 85 |
except Exception as e:
|
| 86 |
print(f"Could not load signature model: {e}")
|
|
@@ -422,9 +438,10 @@ def preview_first_page(file: gr.File):
|
|
| 422 |
pix = page.get_pixmap(matrix=fitz.Matrix(1.5, 1.5))
|
| 423 |
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 424 |
doc.close()
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
|
|
|
| 428 |
else:
|
| 429 |
# For images, return path directly
|
| 430 |
return path
|
|
@@ -446,6 +463,74 @@ def signature_only_with_preview(file, try_scales, conf, iou, augment):
|
|
| 446 |
return preview, img, summ, js
|
| 447 |
|
| 448 |
# -------- Signature-only utilities (full-image, no ROI) --------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
def signature_only_infer(
|
| 450 |
file: gr.File,
|
| 451 |
try_scales: bool,
|
|
@@ -475,24 +560,29 @@ def signature_only_infer(
|
|
| 475 |
all_boxes_mapped = []
|
| 476 |
rh, rw = base_bgr.shape[:2]
|
| 477 |
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
|
| 497 |
# Annotate and prepare outputs
|
| 498 |
annotated = annotate_signature_boxes_on_pil(base_rgb, all_boxes_mapped)
|
|
@@ -620,18 +710,11 @@ with gr.Blocks(title="Document Layout Detection", theme=gr.themes.Soft()) as dem
|
|
| 620 |
outputs=[visualization_output, summary_output, markdown_output, json_output]
|
| 621 |
)
|
| 622 |
|
| 623 |
-
# Preview on file selection
|
| 624 |
-
file_input.change(
|
| 625 |
-
fn=preview_first_page,
|
| 626 |
-
inputs=[file_input],
|
| 627 |
-
outputs=[input_preview]
|
| 628 |
-
)
|
| 629 |
-
|
| 630 |
-
# Auto-process on file upload (optional)
|
| 631 |
file_input.change(
|
| 632 |
-
fn=
|
| 633 |
inputs=[file_input, mode_dropdown, ocr_checkbox, tables_checkbox, run_sig_chk, sig_conf_slider],
|
| 634 |
-
outputs=[visualization_output, summary_output, markdown_output, json_output]
|
| 635 |
)
|
| 636 |
|
| 637 |
with gr.Tab("✍️ Signature Detection (Only)"):
|
|
@@ -682,27 +765,6 @@ with gr.Blocks(title="Document Layout Detection", theme=gr.themes.Soft()) as dem
|
|
| 682 |
outputs=[sig_input_preview]
|
| 683 |
)
|
| 684 |
|
| 685 |
-
gr.Markdown("""
|
| 686 |
-
### Legend
|
| 687 |
-
Different colors represent different document elements:
|
| 688 |
-
|
| 689 |
-
**Layout Elements:**
|
| 690 |
-
- 🔴 Title • 🔵 Text • 🟢 Section Header • 🟠 Table • 🟣 List/Figure/Formula
|
| 691 |
-
|
| 692 |
-
**Picture Classifications (AI-detected):**
|
| 693 |
-
- 🟣 Signature • 🟢 QR Code • 🟢 Barcode • 🟡 Logo • 🔴 Stamp
|
| 694 |
-
- 🟦 Charts (Bar/Pie/Line) • 🟣 Flow Chart • 🟠 Screenshot • ⚪ Other
|
| 695 |
-
|
| 696 |
-
### How to Use
|
| 697 |
-
1. Upload your document (PDF or image of ID card, invoice, report, etc.)
|
| 698 |
-
2. Choose processing options (Fast mode recommended for quick results)
|
| 699 |
-
3. Click "Process Document"
|
| 700 |
-
4. View the visualization with bounding boxes and explore the outputs
|
| 701 |
-
|
| 702 |
-
### 💡 Try Examples Below!
|
| 703 |
-
Click on any example document to see instant results on different document types.
|
| 704 |
-
""")
|
| 705 |
-
|
| 706 |
# Events are now scoped within tabs above
|
| 707 |
|
| 708 |
# Launch the app
|
|
@@ -713,9 +775,10 @@ if __name__ == "__main__":
|
|
| 713 |
load_signature_model()
|
| 714 |
except Exception:
|
| 715 |
pass
|
| 716 |
-
# Gradio v5 uses default_concurrency_limit;
|
|
|
|
| 717 |
try:
|
| 718 |
-
demo.queue(default_concurrency_limit=
|
| 719 |
except TypeError:
|
| 720 |
-
demo.queue(concurrency_count=
|
| 721 |
demo.launch()
|
|
|
|
| 12 |
import numpy as np
|
| 13 |
import cv2
|
| 14 |
from typing import List, Tuple, Optional
|
| 15 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 16 |
+
import threading
|
| 17 |
+
|
| 18 |
+
# Set CPU thread counts for better multi-core utilization
|
| 19 |
+
os.environ['OMP_NUM_THREADS'] = '2'
|
| 20 |
+
os.environ['OPENBLAS_NUM_THREADS'] = '2'
|
| 21 |
+
os.environ['MKL_NUM_THREADS'] = '2'
|
| 22 |
+
os.environ['NUMEXPR_NUM_THREADS'] = '2'
|
| 23 |
|
| 24 |
# Optional imports for signature detection
|
| 25 |
try:
|
|
|
|
| 89 |
token=os.environ.get("HF_TOKEN")
|
| 90 |
)
|
| 91 |
_SIGNATURE_MODEL = YOLO(model_path)
|
| 92 |
+
|
| 93 |
+
# Configure for CPU multi-threading
|
| 94 |
+
try:
|
| 95 |
+
import torch
|
| 96 |
+
torch.set_num_threads(2) # Use both CPU cores
|
| 97 |
+
except Exception:
|
| 98 |
+
pass
|
| 99 |
+
|
| 100 |
return _SIGNATURE_MODEL
|
| 101 |
except Exception as e:
|
| 102 |
print(f"Could not load signature model: {e}")
|
|
|
|
| 438 |
pix = page.get_pixmap(matrix=fitz.Matrix(1.5, 1.5))
|
| 439 |
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 440 |
doc.close()
|
| 441 |
+
# Use delete=True and return immediately - Gradio will handle the file
|
| 442 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
|
| 443 |
+
img.save(tmp.name)
|
| 444 |
+
return tmp.name
|
| 445 |
else:
|
| 446 |
# For images, return path directly
|
| 447 |
return path
|
|
|
|
| 463 |
return preview, img, summ, js
|
| 464 |
|
| 465 |
# -------- Signature-only utilities (full-image, no ROI) --------
|
| 466 |
+
def _apply_nms(boxes, iou_threshold=0.5):
|
| 467 |
+
"""Apply Non-Maximum Suppression to remove duplicate detections."""
|
| 468 |
+
if not boxes:
|
| 469 |
+
return []
|
| 470 |
+
|
| 471 |
+
# Convert to format for NMS: [x1, y1, x2, y2, score]
|
| 472 |
+
boxes_array = np.array([[b[0][0], b[0][1], b[0][2], b[0][3], b[1]] for b in boxes])
|
| 473 |
+
|
| 474 |
+
# Sort by score
|
| 475 |
+
indices = np.argsort(boxes_array[:, 4])[::-1]
|
| 476 |
+
keep = []
|
| 477 |
+
|
| 478 |
+
while len(indices) > 0:
|
| 479 |
+
# Pick the box with highest score
|
| 480 |
+
current = indices[0]
|
| 481 |
+
keep.append(current)
|
| 482 |
+
|
| 483 |
+
if len(indices) == 1:
|
| 484 |
+
break
|
| 485 |
+
|
| 486 |
+
# Compute IoU with remaining boxes
|
| 487 |
+
current_box = boxes_array[current, :4]
|
| 488 |
+
other_boxes = boxes_array[indices[1:], :4]
|
| 489 |
+
|
| 490 |
+
# Calculate intersection
|
| 491 |
+
x1 = np.maximum(current_box[0], other_boxes[:, 0])
|
| 492 |
+
y1 = np.maximum(current_box[1], other_boxes[:, 1])
|
| 493 |
+
x2 = np.minimum(current_box[2], other_boxes[:, 2])
|
| 494 |
+
y2 = np.minimum(current_box[3], other_boxes[:, 3])
|
| 495 |
+
|
| 496 |
+
intersection = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
|
| 497 |
+
|
| 498 |
+
# Calculate union
|
| 499 |
+
current_area = (current_box[2] - current_box[0]) * (current_box[3] - current_box[1])
|
| 500 |
+
other_areas = (other_boxes[:, 2] - other_boxes[:, 0]) * (other_boxes[:, 3] - other_boxes[:, 1])
|
| 501 |
+
union = current_area + other_areas - intersection
|
| 502 |
+
|
| 503 |
+
# Calculate IoU
|
| 504 |
+
iou = intersection / (union + 1e-6)
|
| 505 |
+
|
| 506 |
+
# Keep boxes with IoU less than threshold
|
| 507 |
+
indices = indices[1:][iou < iou_threshold]
|
| 508 |
+
|
| 509 |
+
# Return filtered boxes
|
| 510 |
+
return [boxes[i] for i in keep]
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def _process_single_scale(base_bgr, s, rw, rh, conf, iou, augment):
|
| 514 |
+
"""Process a single scale - used for parallel execution."""
|
| 515 |
+
tw, th = int(rw * s), int(rh * s)
|
| 516 |
+
resized = cv2.resize(base_bgr, (tw, th), interpolation=cv2.INTER_CUBIC)
|
| 517 |
+
boxes = yolo_detect_signatures(resized, imgsz=1280, conf=conf, iou=iou, augment=augment)
|
| 518 |
+
if not boxes:
|
| 519 |
+
return []
|
| 520 |
+
sx, sy = rw / max(1, tw), rh / max(1, th)
|
| 521 |
+
mapped_boxes = []
|
| 522 |
+
for (xyxy, score, cls) in boxes:
|
| 523 |
+
xb1, yb1, xb2, yb2 = xyxy
|
| 524 |
+
# Map back to original image coords
|
| 525 |
+
x1o = xb1 * sx
|
| 526 |
+
y1o = yb1 * sy
|
| 527 |
+
x2o = xb2 * sx
|
| 528 |
+
y2o = yb2 * sy
|
| 529 |
+
mapped = (np.array([x1o, y1o, x2o, y2o]), float(score), int(cls))
|
| 530 |
+
mapped_boxes.append(mapped)
|
| 531 |
+
return mapped_boxes
|
| 532 |
+
|
| 533 |
+
|
| 534 |
def signature_only_infer(
|
| 535 |
file: gr.File,
|
| 536 |
try_scales: bool,
|
|
|
|
| 560 |
all_boxes_mapped = []
|
| 561 |
rh, rw = base_bgr.shape[:2]
|
| 562 |
|
| 563 |
+
# Process scales in parallel if multiple scales
|
| 564 |
+
if len(scales) > 1 and try_scales:
|
| 565 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
| 566 |
+
futures = [
|
| 567 |
+
executor.submit(_process_single_scale, base_bgr, s, rw, rh, conf, iou, augment)
|
| 568 |
+
for s in scales
|
| 569 |
+
]
|
| 570 |
+
for future in futures:
|
| 571 |
+
boxes = future.result()
|
| 572 |
+
all_boxes_mapped.extend(boxes)
|
| 573 |
+
else:
|
| 574 |
+
# Single scale - no threading overhead
|
| 575 |
+
boxes = _process_single_scale(base_bgr, scales[0], rw, rh, conf, iou, augment)
|
| 576 |
+
all_boxes_mapped.extend(boxes)
|
| 577 |
+
|
| 578 |
+
# Apply NMS to remove duplicate detections from different scales
|
| 579 |
+
if len(all_boxes_mapped) > 1:
|
| 580 |
+
all_boxes_mapped = _apply_nms(all_boxes_mapped, iou_threshold=0.5)
|
| 581 |
+
|
| 582 |
+
# Find best detection
|
| 583 |
+
for box in all_boxes_mapped:
|
| 584 |
+
if best is None or box[1] > best[1]:
|
| 585 |
+
best = box
|
| 586 |
|
| 587 |
# Annotate and prepare outputs
|
| 588 |
annotated = annotate_signature_boxes_on_pil(base_rgb, all_boxes_mapped)
|
|
|
|
| 710 |
outputs=[visualization_output, summary_output, markdown_output, json_output]
|
| 711 |
)
|
| 712 |
|
| 713 |
+
# Preview on file selection and auto-process
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
file_input.change(
|
| 715 |
+
fn=analyze_with_preview,
|
| 716 |
inputs=[file_input, mode_dropdown, ocr_checkbox, tables_checkbox, run_sig_chk, sig_conf_slider],
|
| 717 |
+
outputs=[input_preview, visualization_output, summary_output, markdown_output, json_output]
|
| 718 |
)
|
| 719 |
|
| 720 |
with gr.Tab("✍️ Signature Detection (Only)"):
|
|
|
|
| 765 |
outputs=[sig_input_preview]
|
| 766 |
)
|
| 767 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
# Events are now scoped within tabs above
|
| 769 |
|
| 770 |
# Launch the app
|
|
|
|
| 775 |
load_signature_model()
|
| 776 |
except Exception:
|
| 777 |
pass
|
| 778 |
+
# Gradio v5 uses default_concurrency_limit; increase to 4 for better resource utilization
|
| 779 |
+
# With 18GB RAM and 2 CPU cores, we can handle more concurrent requests
|
| 780 |
try:
|
| 781 |
+
demo.queue(default_concurrency_limit=4)
|
| 782 |
except TypeError:
|
| 783 |
+
demo.queue(concurrency_count=4)
|
| 784 |
demo.launch()
|