mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 17:00:23 +00:00
fix: add validated container regions for figure contained text
Add _container_area_ok, _container_has_media_asset, _validated_container_regions helpers and wire validated _container_bbox regions into tag_figure_contained_text. Containment-only fix: no ownership mutation, no new matched_figure entries.
This commit is contained in:
parent
05e75e1b29
commit
916eadb5ff
2 changed files with 151 additions and 1 deletions
|
|
@ -5135,6 +5135,54 @@ _LEAK_ROLES = {
|
|||
}
|
||||
|
||||
|
||||
def _container_area_ok(container_bbox: list[float], page_width: float, page_height: float) -> bool:
|
||||
"""Reject container bboxes that cover >65% of the page."""
|
||||
if len(container_bbox) < 4 or page_width <= 0 or page_height <= 0:
|
||||
return False
|
||||
cw = container_bbox[2] - container_bbox[0]
|
||||
ch = container_bbox[3] - container_bbox[1]
|
||||
page_area = max(1.0, page_width * page_height)
|
||||
container_area = max(1.0, cw * ch)
|
||||
if container_area >= page_area * 0.65:
|
||||
return False
|
||||
if cw >= page_width * 0.98 and ch >= page_height * 0.45:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _container_has_media_asset(container_bbox: list[float], page_blocks: list[dict]) -> bool:
|
||||
"""Check at least one figure/table asset falls inside the container."""
|
||||
for block in page_blocks:
|
||||
if block.get("role") not in {"figure_asset", "media_asset"}:
|
||||
continue
|
||||
bbox = block.get("bbox") or [0, 0, 0, 0]
|
||||
if len(bbox) < 4:
|
||||
continue
|
||||
if _is_contained(bbox, container_bbox):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _validated_container_regions(page_blocks: list[dict], page_width: float, page_height: float) -> list[list[float]]:
|
||||
"""Collect and validate unique _container_bbox values from blocks."""
|
||||
regions: list[list[float]] = []
|
||||
seen: set[tuple[float, float, float, float]] = set()
|
||||
for block in page_blocks:
|
||||
bbox = block.get("_container_bbox")
|
||||
if not bbox or len(bbox) < 4:
|
||||
continue
|
||||
tup = tuple(float(x) for x in bbox)
|
||||
if tup in seen:
|
||||
continue
|
||||
seen.add(tup)
|
||||
if not _container_area_ok(list(tup), page_width, page_height):
|
||||
continue
|
||||
if not _container_has_media_asset(list(tup), page_blocks):
|
||||
continue
|
||||
regions.append(list(tup))
|
||||
return regions
|
||||
|
||||
|
||||
def _cluster_bboxes_by_proximity(
|
||||
bboxes: list[list[float]],
|
||||
margin: float = 40,
|
||||
|
|
@ -5261,6 +5309,12 @@ def tag_figure_contained_text(
|
|||
if region:
|
||||
figure_regions.append(("matched", region))
|
||||
covered_asset_keys |= _matched_asset_keys(mf)
|
||||
# ── Container bbox regions (validated _container_bbox) ──
|
||||
page_width = max((float(b.get("page_width") or 0.0) for b in page_blocks), default=0.0)
|
||||
page_height = max((float(b.get("page_height") or 0.0) for b in page_blocks), default=0.0)
|
||||
for cr in _validated_container_regions(page_blocks, page_width, page_height):
|
||||
if not _highly_overlaps_any_matched_region(cr, figure_regions):
|
||||
figure_regions.append(("container", cr))
|
||||
fallback_assets = [
|
||||
b for b in page_blocks
|
||||
if (int(b.get("page", 0) or 0), str(b.get("block_id", ""))) not in covered_asset_keys
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ from __future__ import annotations
|
|||
|
||||
from paperforge.worker.ocr_figures import (
|
||||
_cluster_bboxes_by_proximity,
|
||||
_container_area_ok,
|
||||
_container_has_media_asset,
|
||||
_highly_overlaps_any_matched_region,
|
||||
_is_contained,
|
||||
_matched_asset_keys,
|
||||
_validated_container_regions,
|
||||
tag_figure_contained_text,
|
||||
)
|
||||
|
||||
|
|
@ -86,7 +89,7 @@ class TestMatchedAssetKeys:
|
|||
|
||||
class TestTagFigureContainedText:
|
||||
def _block(self, bid, page, x1, y1, x2, y2, role="body_paragraph",
|
||||
raw_label="", asset_family_hint=""):
|
||||
raw_label="", asset_family_hint="", **kwargs):
|
||||
b = {
|
||||
"block_id": bid,
|
||||
"page": page,
|
||||
|
|
@ -97,6 +100,7 @@ class TestTagFigureContainedText:
|
|||
b["raw_label"] = raw_label
|
||||
if asset_family_hint:
|
||||
b["asset_family_hint"] = asset_family_hint
|
||||
b.update(kwargs)
|
||||
return b
|
||||
|
||||
def _matched_fig(self, page, cluster_bbox, matched_assets=None):
|
||||
|
|
@ -176,3 +180,95 @@ class TestTagFigureContainedText:
|
|||
blocks = [block, self._block("a1", 1, 50, 50, 400, 400, role="figure_asset")]
|
||||
tag_figure_contained_text(blocks, [mf])
|
||||
assert block["role"] == "figure_inner_text"
|
||||
|
||||
def test_container_bbox_tags_vision_footnote_inside_demoted_figure(self):
|
||||
blocks = [
|
||||
self._block(
|
||||
"inner", 1, 120, 120, 180, 140,
|
||||
role="footnote", text="Single outlet",
|
||||
_container_bbox=[90, 90, 320, 220],
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
self._block(
|
||||
"asset1", 1, 100, 100, 200, 200,
|
||||
role="media_asset", raw_label="table",
|
||||
asset_family_hint="ambiguous",
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
self._block(
|
||||
"asset2", 1, 210, 100, 310, 200,
|
||||
role="media_asset", raw_label="table",
|
||||
asset_family_hint="ambiguous",
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
]
|
||||
tag_figure_contained_text(blocks, [])
|
||||
assert blocks[0]["role"] == "figure_inner_text"
|
||||
assert blocks[0]["_figure_contained"] is True
|
||||
|
||||
def test_container_bbox_does_not_consume_body_paragraph_next_to_figure(self):
|
||||
blocks = [
|
||||
self._block(
|
||||
"body", 1, 350, 100, 700, 130,
|
||||
role="body_paragraph", text="Nearby body text",
|
||||
_container_bbox=[90, 90, 320, 220],
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
self._block(
|
||||
"asset1", 1, 100, 100, 200, 200,
|
||||
role="media_asset", raw_label="image",
|
||||
asset_family_hint="figure_like",
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
]
|
||||
tag_figure_contained_text(blocks, [])
|
||||
assert blocks[0]["role"] == "body_paragraph"
|
||||
assert not blocks[0].get("_figure_contained")
|
||||
|
||||
def test_huge_container_bbox_is_rejected_by_area_gate(self):
|
||||
blocks = [
|
||||
self._block(
|
||||
"body", 1, 100, 100, 300, 130,
|
||||
role="body_paragraph", text="Nearby body text",
|
||||
_container_bbox=[0, 0, 1000, 1600],
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
self._block(
|
||||
"asset", 1, 100, 500, 300, 700,
|
||||
role="media_asset", raw_label="image",
|
||||
asset_family_hint="figure_like",
|
||||
page_width=1000, page_height=1600,
|
||||
),
|
||||
]
|
||||
tag_figure_contained_text(blocks, [])
|
||||
assert blocks[0]["role"] == "body_paragraph"
|
||||
|
||||
def test_validated_container_regions_reject_missing_media_assets(self):
|
||||
blocks = [
|
||||
self._block("body", 1, 100, 100, 300, 130, role="body_paragraph", text="x",
|
||||
_container_bbox=[90, 90, 320, 220], page_width=1000, page_height=1600),
|
||||
]
|
||||
regions = _validated_container_regions(blocks, 1000, 1600)
|
||||
assert regions == []
|
||||
|
||||
def test_validated_container_regions_accept_reasonable_container_with_media(self):
|
||||
blocks = [
|
||||
self._block("body", 1, 120, 120, 180, 140, role="footnote", text="x",
|
||||
_container_bbox=[90, 90, 320, 220], page_width=1000, page_height=1600),
|
||||
self._block("asset", 1, 100, 100, 200, 200, role="media_asset",
|
||||
raw_label="image", asset_family_hint="figure_like",
|
||||
page_width=1000, page_height=1600),
|
||||
]
|
||||
regions = _validated_container_regions(blocks, 1000, 1600)
|
||||
assert regions == [[90, 90, 320, 220]]
|
||||
|
||||
def test_matched_cluster_bbox_takes_precedence_over_container_bbox(self):
|
||||
mf = self._matched_fig(1, [80, 80, 220, 220], matched_assets=[{"block_id": "a1", "bbox": [90, 90, 200, 200]}])
|
||||
blocks = [
|
||||
self._block("inner", 1, 120, 120, 180, 140, role="footnote", text="x",
|
||||
_container_bbox=[0, 0, 1000, 1600], page_width=1000, page_height=1600),
|
||||
self._block("a1", 1, 90, 90, 200, 200, role="figure_asset", raw_label="image",
|
||||
asset_family_hint="figure_like", page_width=1000, page_height=1600),
|
||||
]
|
||||
tag_figure_contained_text(blocks, [mf])
|
||||
assert blocks[0]["role"] == "figure_inner_text"
|
||||
|
|
|
|||
Loading…
Reference in a new issue