fix: prioritize formal figure legends over panel subcaptions

This commit is contained in:
Research Assistant 2026-06-06 16:01:57 +08:00
parent 69a1156449
commit cb3f459ba2
2 changed files with 86 additions and 18 deletions

View file

@ -85,6 +85,9 @@ def _is_body_mention(block: dict) -> bool:
return False
_PANEL_SUBCAPTION_PATTERN = re.compile(r"^\s*[a-z][\.\)]\s")
def _is_formal_legend(text: str, block: dict | None = None, page_width: float = 1200) -> bool:
if not text:
return False
@ -160,7 +163,7 @@ def _is_formal_legend(text: str, block: dict | None = None, page_width: float =
if text_len < 100 and words and words.issubset(axis_words | stop_words):
return False
return True
return not (_PANEL_SUBCAPTION_PATTERN.match(text) and not _FIGURE_NUMBER_PATTERN.search(text))
def _cluster_bbox(bboxes: list[list[float]]) -> list[float]:
@ -307,10 +310,14 @@ def build_figure_inventory(structured_blocks: list[dict], page_width: float = 12
if raw_label in {"image", "chart", "figure_title", "figure"} or not raw_label:
assets.append(block)
numbered_legends = [leg for leg in legends if _extract_figure_number(leg.get("text", "")) is not None]
unnumbered_legends = [leg for leg in legends if _extract_figure_number(leg.get("text", "")) is None]
ordered_legends = numbered_legends + unnumbered_legends
candidate_regions = _compute_candidate_figure_regions(structured_blocks, page_width)
used_asset_indices: set[int] = set()
used_region_ids: set[str] = set()
for legend in legends:
for legend in ordered_legends:
legend_page = legend.get("page", 0)
legend_text = legend.get("text", "")
fig_num = _extract_figure_number(legend_text)
@ -336,9 +343,8 @@ def build_figure_inventory(structured_blocks: list[dict], page_width: float = 12
used_region_ids.add(region_match["region_id"])
for asset in matched_assets:
for i, candidate in enumerate(assets):
if (
candidate.get("block_id") == asset.get("block_id")
and candidate.get("page", 0) == asset.get("page", 0)
if candidate.get("block_id") == asset.get("block_id") and candidate.get("page", 0) == asset.get(
"page", 0
):
used_asset_indices.add(i)
break
@ -385,23 +391,24 @@ def build_figure_inventory(structured_blocks: list[dict], page_width: float = 12
for asset in media_blocks:
for i, candidate in enumerate(assets):
if (
candidate.get("block_id") == asset.get("block_id")
and candidate.get("page", 0) == asset.get("page", 0)
if candidate.get("block_id") == asset.get("block_id") and candidate.get("page", 0) == asset.get(
"page", 0
):
used_asset_indices.add(i)
break
unresolved_clusters.append({
"cluster_id": f"cluster_{len(unresolved_clusters) + 1:03d}",
"page": page,
"bbox": bbox,
"media_block_ids": media_block_ids,
"matched_legend_block_id": None,
"status": "unresolved_multi_panel",
"confidence": 0.45,
"flags": ["legend_rejected", "multi_panel_cluster"],
})
unresolved_clusters.append(
{
"cluster_id": f"cluster_{len(unresolved_clusters) + 1:03d}",
"page": page,
"bbox": bbox,
"media_block_ids": media_block_ids,
"matched_legend_block_id": None,
"status": "unresolved_multi_panel",
"confidence": 0.45,
"flags": ["legend_rejected", "multi_panel_cluster"],
}
)
for i, asset in enumerate(assets):
if i not in used_asset_indices:

View file

@ -591,3 +591,64 @@ def test_unresolved_clusters_in_inventory() -> None:
assert len(inventory["unmatched_assets"]) == 0, (
"Six panels should be consumed by unresolved cluster, not left as individual unmatched assets"
)
# === panel subcaption / formal legend precedence ===
def test_panel_subcaption_rejected_by_is_formal_legend() -> None:
from paperforge.worker.ocr_figures import _is_formal_legend
panel_subcaption = "c. Redistribution of cells after 24 hours of treatment"
assert _is_formal_legend(panel_subcaption) is False
def test_panel_subcaption_with_parenthesis_rejected() -> None:
from paperforge.worker.ocr_figures import _is_formal_legend
panel_subcaption = "a) Control group measurements at 48 hours post stimulation"
assert _is_formal_legend(panel_subcaption) is False
def test_formal_legend_precedence_over_panel_subcaption() -> None:
"""When a numbered formal legend and a panel subcaption exist on the same page
with a single candidate region, the formal legend must claim the region."""
from paperforge.worker.ocr_figures import build_figure_inventory
structured_blocks = [
{
"paper_id": "K001",
"page": 1,
"block_id": "p1_b1",
"role": "figure_caption",
"text": "Fig. 2. Quantitative analysis of cell migration under DC electric field "
"stimulation over 48 hours.",
"bbox": [50, 420, 550, 460],
},
{
"paper_id": "K001",
"page": 1,
"block_id": "p1_b2",
"role": "figure_caption",
"text": "c. Redistribution of cells after 24 hours of treatment with "
"osteogenic medium for differentiation induction",
"bbox": [60, 350, 540, 380],
},
{
"paper_id": "K001",
"page": 1,
"block_id": "p1_b3",
"role": "figure_asset",
"text": "",
"bbox": [60, 50, 540, 330],
},
]
inventory = build_figure_inventory(structured_blocks)
assert len(inventory["matched_figures"]) == 1
assert inventory["matched_figures"][0]["figure_number"] == 2
assert len(inventory["matched_figures"][0]["matched_assets"]) == 1
assert len(inventory["rejected_legends"]) == 1
assert "c. Redistribution" in inventory["rejected_legends"][0].get("text", "")