fix: refine thin-border rect filter to only skip dark-gray decorations

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.
This commit is contained in:
Research Assistant 2026-06-26 19:16:37 +08:00
parent 1e1acebbe8
commit 190ce2440a
3 changed files with 50 additions and 7 deletions

View file

@ -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)

View file

@ -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

View file

@ -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