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:
LLLin000 2026-07-01 21:50:37 +08:00
parent 05e75e1b29
commit 916eadb5ff
2 changed files with 151 additions and 1 deletions

View file

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

View file

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