mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
fix: prioritize formal figure legends over panel subcaptions
This commit is contained in:
parent
69a1156449
commit
cb3f459ba2
2 changed files with 86 additions and 18 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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", "")
|
||||
|
|
|
|||
Loading…
Reference in a new issue