mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
fix: sidecar figure caption preservation + rotated table caption matching
37LK5T97 had two bugs:
1. Figure 1 sidecar demotion (ocr_document.py):
- caption in left column (246px wide), image in right column
- _is_near_figure_media uses h_overlap which fails for adjacent columns
- candidate_resolution demoted the caption as 'narrative prose'
- Fix: add _is_sidecar_candidate check — if figure_caption_candidate
has vertical overlap with a media_asset but no horizontal overlap
(adjacent columns), skip the prose-based demotion.
- New narrow helper, does not change _is_near_figure_media (avoids
regression in vision_footnote routing)
2. Rotated table caption matching (ocr_scores.py):
- Tables 1-3 had rotated captions (dir=[0,-1], vertical text)
- score_table_match only checked x_overlap + asset_below_caption
- Rotated captions are beside the table body, not above it
- Fix: when caption has rotated text and x_overlap < 0.5, use
adjacent-column gap check + y_overlap ratio instead
- Non-rotated captions unaffected (elif only fires for rotated)
Result: 37LK5T97 Figure 1 matched (figure_001, asset=block_id=9).
All 6 tables (3 rotated + 1 continuation x2 + 1 normal) matched.
428 regression tests pass.
This commit is contained in:
parent
1cff29ff01
commit
bd3f3b63e9
2 changed files with 56 additions and 0 deletions
|
|
@ -4332,6 +4332,27 @@ def _check_caption_style_match(block: dict, blocks: list[dict]) -> bool:
|
|||
return size_match and font_match
|
||||
|
||||
|
||||
def _is_sidecar_candidate(block: dict, page_blocks: list[dict]) -> bool:
|
||||
"""Check if a figure_caption_candidate is a sidecar: no x_overlap but y_overlap
|
||||
with a media_asset on the same page (adjacent columns layout)."""
|
||||
bb = block.get("bbox") or block.get("block_bbox") or [0, 0, 0, 0]
|
||||
if len(bb) < 4:
|
||||
return False
|
||||
bx1, by1, bx2, by2 = bb[:4]
|
||||
for other in page_blocks:
|
||||
label = str(other.get("block_label") or other.get("raw_label") or "").strip()
|
||||
if label not in {"image", "chart", "figure"}:
|
||||
continue
|
||||
ob = other.get("bbox") or other.get("block_bbox") or []
|
||||
if len(ob) < 4:
|
||||
continue
|
||||
ox1, oy1, ox2, oy2 = ob[:4]
|
||||
# Adjacent columns: no horizontal overlap but vertical overlap
|
||||
if not (bx1 < ox2 and ox1 < bx2) and (by1 < oy2 and oy1 < by2):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_ambiguous_candidates(
|
||||
blocks: list[dict],
|
||||
doc_structure: DocumentStructure,
|
||||
|
|
@ -4535,6 +4556,7 @@ def _resolve_ambiguous_candidates(
|
|||
reason="backmatter heading candidate in backmatter region, promoted to backmatter heading",
|
||||
)
|
||||
|
||||
|
||||
# ---- 2.2 Resolve figure_caption_candidate ----
|
||||
if role == "figure_caption_candidate":
|
||||
text = block.get("text", "") or ""
|
||||
|
|
@ -4556,6 +4578,9 @@ def _resolve_ambiguous_candidates(
|
|||
if (
|
||||
in_body_spine
|
||||
and is_prose
|
||||
# Sidecar figure captions (caption in one column, image adjacent)
|
||||
# must survive prose detection — they are long by nature.
|
||||
and not _is_sidecar_candidate(block, page_blocks)
|
||||
and not (zone == "display_zone" and style_family == "legend_like" and raw_label == "figure_title")
|
||||
# Vision-footnote figure descriptions already validated by dedicated rescue
|
||||
and not (raw_label == "vision_footnote" and _looks_like_figure_description_opening(text))
|
||||
|
|
|
|||
|
|
@ -65,6 +65,37 @@ def score_table_match(caption: dict, asset: dict, *, is_continuation: bool = Fal
|
|||
if _bbox_x_overlap_ratio(caption_bbox, asset_bbox) >= 0.5:
|
||||
score += 0.25
|
||||
evidence.append("x_overlap")
|
||||
elif len(caption_bbox) >= 4 and len(asset_bbox) >= 4:
|
||||
# Check if caption has rotated text (dir != [1,0]) — vertical caption
|
||||
# placed beside the table body, not above it.
|
||||
span = caption.get("span_metadata") or []
|
||||
if isinstance(span, dict):
|
||||
span = [span]
|
||||
_is_rotated = False
|
||||
if isinstance(span, list):
|
||||
for _entry in span:
|
||||
if not isinstance(_entry, dict):
|
||||
continue
|
||||
_dir = _entry.get("dir")
|
||||
if isinstance(_dir, (list, tuple)) and len(_dir) == 2:
|
||||
if abs(float(_dir[1])) > abs(float(_dir[0])):
|
||||
_is_rotated = True
|
||||
break
|
||||
if _is_rotated:
|
||||
# Adjacent-column check: caption beside table, no x_overlap
|
||||
_gap = max(0.0, asset_bbox[0] - caption_bbox[2], caption_bbox[0] - asset_bbox[2])
|
||||
_narrow_w = min(caption_bbox[2] - caption_bbox[0], asset_bbox[2] - asset_bbox[0])
|
||||
if _gap < _narrow_w * 0.3 and _gap < 80:
|
||||
score += 0.20
|
||||
evidence.append("adjacent_x")
|
||||
# y_overlap: caption and table share vertical band
|
||||
_by1, _by2 = caption_bbox[1], caption_bbox[3]
|
||||
_oy1, _oy2 = asset_bbox[1], asset_bbox[3]
|
||||
_y_overlap = max(0.0, min(_by2, _oy2) - max(_by1, _oy1))
|
||||
_min_span = min(_by2 - _by1, _oy2 - _oy1)
|
||||
if _min_span > 0 and _y_overlap / _min_span >= 0.3:
|
||||
score += 0.15
|
||||
evidence.append("y_overlap_with_asset")
|
||||
if len(caption_bbox) >= 4 and len(asset_bbox) >= 4:
|
||||
if same_page and asset_bbox[1] >= caption_bbox[3]:
|
||||
score += 0.25
|
||||
|
|
|
|||
Loading…
Reference in a new issue