From 190ce2440aaba4743e31f7bcb2f4d253ed560ca9 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Fri, 26 Jun 2026 19:16:37 +0800 Subject: [PATCH] fix: refine thin-border rect filter to only skip dark-gray decorations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skip only thin-border unfilled rects with near-gray color and low brightness — these are PyMuPDF get_drawings() false positives from clipping paths. Keep colored thin borders (legitimate boxes) and pure black thin borders (legitimate black frames). Added 2 tests (colored border kept, black border kept). Verified via 50-paper audit: all 14 dark-gray rects filtered, all colored/black rects preserved. --- paperforge/worker/ocr_pdf_spans.py | 17 ++++++++----- paperforge/worker/ocr_rebuild.py | 2 +- tests/test_ocr_pdf_spans.py | 38 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/paperforge/worker/ocr_pdf_spans.py b/paperforge/worker/ocr_pdf_spans.py index 30d22221..bd6a15c6 100644 --- a/paperforge/worker/ocr_pdf_spans.py +++ b/paperforge/worker/ocr_pdf_spans.py @@ -125,7 +125,6 @@ def _extract_visual_container_rects(page: Any) -> list: except Exception: return rects - page_area = (page.rect.width or 1) * (page.rect.height or 1) for drawing in drawings: fill = drawing.get("fill") color = drawing.get("color") @@ -145,12 +144,18 @@ def _extract_visual_container_rects(page: Any) -> list: has_border = bool(color) and (isinstance(color, (list, tuple))) and stroke_width > 0 - # Skip thin-bordered, unfilled rectangles that cover >50% of page — - # these are page decoration lines (e.g. text area frame), not callout containers. + # Skip thin-bordered, unfilled rectangles in dark near-gray color — + # these are PDF layout/drawing frame lines (text area borders, figure + # borders, clipping paths returned as drawings by PyMuPDF), not callout + # containers. Real callout boxes have a colored fill, a thick/colored + # border (>1pt), or a true black border (pure RGB 0,0,0). if has_border and not is_filled and stroke_width <= 1.0: - coverage = (rect.width * rect.height) / page_area - if coverage > 0.5: - continue + if color and len(color) >= 3: + r, g, b = color[0], color[1], color[2] + is_gray = max(r, g, b) - min(r, g, b) < 0.05 + brightness = (r + g + b) / 3 + if is_gray and 0.02 < brightness < 0.3: + continue if is_filled or has_border: rects.append(rect) diff --git a/paperforge/worker/ocr_rebuild.py b/paperforge/worker/ocr_rebuild.py index 1e5cdb1b..73dcf8e6 100644 --- a/paperforge/worker/ocr_rebuild.py +++ b/paperforge/worker/ocr_rebuild.py @@ -7,7 +7,7 @@ from pathlib import Path from paperforge.core.io import read_json, write_json CURRENT_SPAN_BACKFILL_VERSION = "2026-06-22.1" -CURRENT_SPAN_VISUAL_CONTAINER_VERSION = "2026-06-26.2" +CURRENT_SPAN_VISUAL_CONTAINER_VERSION = "2026-06-26.4" MIN_SPAN_BACKFILL_COVERAGE = 0.90 diff --git a/tests/test_ocr_pdf_spans.py b/tests/test_ocr_pdf_spans.py index 76d18754..2ae986a3 100644 --- a/tests/test_ocr_pdf_spans.py +++ b/tests/test_ocr_pdf_spans.py @@ -188,6 +188,44 @@ def test_extract_visual_container_detects_thick_border_box() -> None: assert len(rects) == 1, "Thick border box should be detected" +def test_extract_visual_container_keeps_colored_thin_border() -> None: + """A colored (non-gray) thin-border rectangle should still be detected.""" + import fitz + + from paperforge.worker.ocr_pdf_spans import _extract_visual_container_rects + + doc = fitz.open() + page = doc.new_page(width=612, height=792) + shape = page.new_shape() + shape.draw_rect(fitz.Rect(400, 600, 550, 650)) # magenta colored thin border + shape.finish(fill=None, color=(0.6, 0.2, 0.6), width=0.6) + shape.commit() + + rects = _extract_visual_container_rects(page) + doc.close() + + assert len(rects) == 1, "Colored thin border should be detected" + + +def test_extract_visual_container_keeps_black_thin_border() -> None: + """A pure black thin-border rectangle should still be detected.""" + import fitz + + from paperforge.worker.ocr_pdf_spans import _extract_visual_container_rects + + doc = fitz.open() + page = doc.new_page(width=612, height=792) + shape = page.new_shape() + shape.draw_rect(fitz.Rect(400, 600, 550, 650)) + shape.finish(fill=None, color=(0, 0, 0), width=0.5) + shape.commit() + + rects = _extract_visual_container_rects(page) + doc.close() + + assert len(rects) == 1, "Black thin border should be detected" + + def test_extract_visual_container_skips_small_thin_border() -> None: """A small thin-border rectangle below min size should be skipped.""" import fitz