mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
469 lines
18 KiB
Text
469 lines
18 KiB
Text
|
|
=== def _bbox_center_x ===
|
||
|
|
def _bbox_center_x(bbox: list[int]) -> float:
|
||
|
|
return (int(bbox[0]) + int(bbox[2])) / 2
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _bbox_center_y ===
|
||
|
|
def _bbox_center_y(bbox: list[int]) -> float:
|
||
|
|
return (int(bbox[1]) + int(bbox[3])) / 2
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _bbox_height ===
|
||
|
|
def _bbox_height(bbox: list[int]) -> int:
|
||
|
|
return max(0, int(bbox[3]) - int(bbox[1]))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _bbox_horizontal_overlap ===
|
||
|
|
def _bbox_horizontal_overlap(a: list[int], b: list[int]) -> int:
|
||
|
|
return max(0, min(int(a[2]), int(b[2])) - max(int(a[0]), int(b[0])))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _bbox_horizontal_overlap_ratio ===
|
||
|
|
def _bbox_horizontal_overlap_ratio(a: list[int], b: list[int]) -> float:
|
||
|
|
width = min(_bbox_width(a), _bbox_width(b))
|
||
|
|
if width <= 0:
|
||
|
|
return 0.0
|
||
|
|
return _bbox_horizontal_overlap(a, b) / width
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _bbox_vertical_overlap ===
|
||
|
|
def _bbox_vertical_overlap(a: list[int], b: list[int]) -> int:
|
||
|
|
return max(0, min(int(a[3]), int(b[3])) - max(int(a[1]), int(b[1])))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _bbox_width ===
|
||
|
|
def _bbox_width(bbox: list[int]) -> int:
|
||
|
|
return max(0, int(bbox[2]) - int(bbox[0]))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _cluster_bbox ===
|
||
|
|
def _cluster_bbox(cluster: list[dict]) -> list[int]:
|
||
|
|
return [
|
||
|
|
min(int(item["block_bbox"][0]) for item in cluster),
|
||
|
|
min(int(item["block_bbox"][1]) for item in cluster),
|
||
|
|
max(int(item["block_bbox"][2]) for item in cluster),
|
||
|
|
max(int(item["block_bbox"][3]) for item in cluster),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _figure_caption_blocks ===
|
||
|
|
def _figure_caption_blocks(blocks: list[dict]) -> list[dict]:
|
||
|
|
captions = []
|
||
|
|
for block in blocks:
|
||
|
|
if block.get("block_label") not in {"figure_title", "paragraph_title", "text"}:
|
||
|
|
continue
|
||
|
|
text = clean_block_text(block.get("block_content", ""))
|
||
|
|
if is_formal_figure_legend(text):
|
||
|
|
captions.append(block)
|
||
|
|
return captions
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def _precaption_media_region ===
|
||
|
|
def _precaption_media_region(
|
||
|
|
caption_bbox: list[int],
|
||
|
|
cluster_bboxes: list[list[int]],
|
||
|
|
blocks: list[dict] | None = None,
|
||
|
|
body_column_width: int = 0,
|
||
|
|
|
||
|
|
=== def _union_bboxes ===
|
||
|
|
def _union_bboxes(bbox_list: list[list[int]]) -> list[int]:
|
||
|
|
return [
|
||
|
|
min(int(bbox[0]) for bbox in bbox_list),
|
||
|
|
min(int(bbox[1]) for bbox in bbox_list),
|
||
|
|
max(int(bbox[2]) for bbox in bbox_list),
|
||
|
|
max(int(bbox[3]) for bbox in bbox_list),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def clean_block_text ===
|
||
|
|
def clean_block_text(text: str) -> str:
|
||
|
|
text = html.unescape(normalize_obsidian_markdown(text)).strip()
|
||
|
|
return text
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def compute_precaption_composite_regions ===
|
||
|
|
def compute_precaption_composite_regions(blocks: list[dict], page_width: int = 0, page_height: int = 0) -> list[dict]:
|
||
|
|
caption_blocks = _figure_caption_blocks(blocks)
|
||
|
|
_, clusters = media_clusters(blocks)
|
||
|
|
cluster_bboxes = [_cluster_bbox(cluster) for cluster in clusters]
|
||
|
|
body_column_width = estimate_body_column_width(blocks, page_width=page_width)
|
||
|
|
regions: list[dict] = []
|
||
|
|
for caption in caption_blocks:
|
||
|
|
caption_bbox = [int(value) for value in caption.get("block_bbox", [0, 0, 0, 0])]
|
||
|
|
precaption_region = _precaption_media_region(
|
||
|
|
caption_bbox, cluster_bboxes, blocks=blocks, body_column_width=body_column_width
|
||
|
|
)
|
||
|
|
if not precaption_region:
|
||
|
|
continue
|
||
|
|
region_blocks = []
|
||
|
|
for block in blocks:
|
||
|
|
label = block.get("block_label", "")
|
||
|
|
bbox = [int(value) for value in block.get("block_bbox", [0, 0, 0, 0])]
|
||
|
|
if bbox[3] > caption_bbox[1] + 24:
|
||
|
|
continue
|
||
|
|
if bbox[3] < precaption_region[1] - 240:
|
||
|
|
continue
|
||
|
|
vertical_overlap_with_region = _bbox_vertical_overlap(bbox, precaption_region)
|
||
|
|
near_region_side = vertical_overlap_with_region > 0 and (
|
||
|
|
0 <= precaption_region[0] - bbox[2] <= 80 or 0 <= bbox[0] - precaption_region[2] <= 80
|
||
|
|
)
|
||
|
|
intersects_region = (
|
||
|
|
_bbox_horizontal_overlap(bbox, precaption_region) > 0
|
||
|
|
or precaption_region[0] - 24 <= _bbox_center_x(bbox) <= precaption_region[2] + 24
|
||
|
|
or near_region_side
|
||
|
|
)
|
||
|
|
if not intersects_region:
|
||
|
|
continue
|
||
|
|
if label in {"image", "chart"}:
|
||
|
|
if _bbox_vertical_overlap(bbox, precaption_region) <= 0 and bbox[3] < precaption_region[1] - 80:
|
||
|
|
continue
|
||
|
|
region_blocks.append(block)
|
||
|
|
continue
|
||
|
|
if label in {"text", "paragraph_title"}:
|
||
|
|
if bbox[3] < precaption_region[1] - 80:
|
||
|
|
continue
|
||
|
|
width = _bbox_width(bbox)
|
||
|
|
text = clean_block_text(block.get("block_content", ""))
|
||
|
|
if is_body_paragraph_like_text_block(
|
||
|
|
block,
|
||
|
|
body_column_width=body_column_width,
|
||
|
|
cluster_bboxes=cluster_bboxes,
|
||
|
|
caption_bbox=caption_bbox,
|
||
|
|
):
|
||
|
|
continue
|
||
|
|
if (
|
||
|
|
text
|
||
|
|
and (
|
||
|
|
not re.match(
|
||
|
|
"^(?:Extended\\s+Data\\s+Fig\\.?|Extended\\s+Data\\s+Figure|Figure|Fig\\.?|Table)\\s+\\w+",
|
||
|
|
text,
|
||
|
|
flags=re.IGNORECASE,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
and (
|
||
|
|
width <= int(max(body_column_width, 1) * 0.78)
|
||
|
|
or is_embedded_figure_text_block(block, blocks, page_width=page_width, page_height=page_height)
|
||
|
|
)
|
||
|
|
):
|
||
|
|
region_blocks.append(block)
|
||
|
|
media_ids = {block.get("block_id") for block in region_blocks if block.get("block_label") in {"image", "chart"}}
|
||
|
|
text_ids = {
|
||
|
|
block.get("block_id") for block in region_blocks if block.get("block_label") in {"text", "paragraph_title"}
|
||
|
|
}
|
||
|
|
if len(media_ids) < 1 or not text_ids or len(region_blocks) < 3:
|
||
|
|
continue
|
||
|
|
region_bbox = [
|
||
|
|
min(int(block["block_bbox"][0]) for block in region_blocks),
|
||
|
|
min(int(block["block_bbox"][1]) for block in region_blocks),
|
||
|
|
max(int(block["block_bbox"][2]) for block in region_blocks),
|
||
|
|
max(int(block["block_bbox"][3]) for block in region_blocks),
|
||
|
|
]
|
||
|
|
regions.append(
|
||
|
|
{
|
||
|
|
"bbox": region_bbox,
|
||
|
|
"block_ids": {block.get("block_id") for block in region_blocks},
|
||
|
|
"caption_block_id": caption.get("block_id"),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return regions
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def estimate_body_column_width ===
|
||
|
|
def estimate_body_column_width(blocks: list[dict], page_width: int = 0) -> int:
|
||
|
|
widths: list[int] = []
|
||
|
|
for block in blocks:
|
||
|
|
if block.get("block_label") not in {"text", "paragraph_title", "abstract"}:
|
||
|
|
continue
|
||
|
|
text = clean_block_text(block.get("block_content", ""))
|
||
|
|
if (
|
||
|
|
not text
|
||
|
|
or is_subfigure_label(text)
|
||
|
|
or re.match("^(?:Figure|Fig\\.?|Table)\\s+\\w+", text, flags=re.IGNORECASE)
|
||
|
|
):
|
||
|
|
continue
|
||
|
|
bbox = [int(value) for value in block.get("block_bbox", [0, 0, 0, 0])]
|
||
|
|
width = _bbox_width(bbox)
|
||
|
|
height = _bbox_height(bbox)
|
||
|
|
if width <= 0 or height <= 0:
|
||
|
|
continue
|
||
|
|
if page_width and width >= int(page_width * 0.82):
|
||
|
|
widths.append(width)
|
||
|
|
continue
|
||
|
|
if width >= 430:
|
||
|
|
widths.append(width)
|
||
|
|
if not widths:
|
||
|
|
return int(page_width * 0.45) if page_width else 520
|
||
|
|
widths.sort()
|
||
|
|
return widths[len(widths) // 2]
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def is_body_paragraph_like_text_block ===
|
||
|
|
def is_body_paragraph_like_text_block(
|
||
|
|
block: dict,
|
||
|
|
body_column_width: int = 0,
|
||
|
|
cluster_bboxes: list[list[int]] | None = None,
|
||
|
|
caption_bbox: list[int] | None = None,
|
||
|
|
|
||
|
|
=== def is_embedded_figure_text_block ===
|
||
|
|
def is_embedded_figure_text_block(block: dict, blocks: list[dict], page_width: int = 0, page_height: int = 0) -> bool:
|
||
|
|
label = block.get("block_label", "")
|
||
|
|
if label not in {"text", "paragraph_title"}:
|
||
|
|
return False
|
||
|
|
if label == "paragraph_title":
|
||
|
|
return False
|
||
|
|
text = clean_block_text(block.get("block_content", ""))
|
||
|
|
if not text:
|
||
|
|
return False
|
||
|
|
if is_formal_figure_legend(text):
|
||
|
|
return False
|
||
|
|
bbox = [int(value) for value in block.get("block_bbox", [0, 0, 0, 0])]
|
||
|
|
width = _bbox_width(bbox)
|
||
|
|
height = _bbox_height(bbox)
|
||
|
|
if width <= 0 or height <= 0:
|
||
|
|
return False
|
||
|
|
if label == "paragraph_title" and is_subfigure_label(text):
|
||
|
|
return True
|
||
|
|
caption_blocks = _figure_caption_blocks(blocks)
|
||
|
|
cluster_index, clusters = media_clusters(blocks)
|
||
|
|
cluster_bboxes = [_cluster_bbox(cluster) for cluster in clusters]
|
||
|
|
body_column_width = estimate_body_column_width(blocks, page_width=page_width)
|
||
|
|
del cluster_index
|
||
|
|
nearest_media = None
|
||
|
|
nearest_media_distance = None
|
||
|
|
close_media_count = 0
|
||
|
|
stacked_media_above = False
|
||
|
|
stacked_media_below = False
|
||
|
|
side_media = False
|
||
|
|
for cluster_bbox in cluster_bboxes:
|
||
|
|
horizontal_ratio = _bbox_horizontal_overlap_ratio(bbox, cluster_bbox)
|
||
|
|
vertical_overlap = _bbox_vertical_overlap(bbox, cluster_bbox)
|
||
|
|
top_gap = int(bbox[1]) - int(cluster_bbox[3])
|
||
|
|
bottom_gap = int(cluster_bbox[1]) - int(bbox[3])
|
||
|
|
center_inside_x = int(cluster_bbox[0]) <= _bbox_center_x(bbox) <= int(cluster_bbox[2])
|
||
|
|
center_inside_y = int(cluster_bbox[1]) <= _bbox_center_y(bbox) <= int(cluster_bbox[3])
|
||
|
|
if (
|
||
|
|
horizontal_ratio >= 0.45
|
||
|
|
and (0 <= top_gap <= 48 or 0 <= bottom_gap <= 48 or vertical_overlap > 0)
|
||
|
|
or (center_inside_x and abs(_bbox_center_y(bbox) - _bbox_center_y(cluster_bbox)) <= max(80, height * 4))
|
||
|
|
):
|
||
|
|
close_media_count += 1
|
||
|
|
if horizontal_ratio >= 0.5 and 0 <= top_gap <= 90:
|
||
|
|
stacked_media_above = True
|
||
|
|
if horizontal_ratio >= 0.5 and 0 <= bottom_gap <= 90:
|
||
|
|
stacked_media_below = True
|
||
|
|
if vertical_overlap > 0 and (
|
||
|
|
0 <= int(bbox[0]) - int(cluster_bbox[2]) <= 60 or 0 <= int(cluster_bbox[0]) - int(bbox[2]) <= 60
|
||
|
|
):
|
||
|
|
side_media = True
|
||
|
|
dx = 0
|
||
|
|
if int(bbox[2]) < int(cluster_bbox[0]):
|
||
|
|
dx = int(cluster_bbox[0]) - int(bbox[2])
|
||
|
|
elif int(cluster_bbox[2]) < int(bbox[0]):
|
||
|
|
dx = int(bbox[0]) - int(cluster_bbox[2])
|
||
|
|
dy = 0
|
||
|
|
if int(bbox[3]) < int(cluster_bbox[1]):
|
||
|
|
dy = int(cluster_bbox[1]) - int(bbox[3])
|
||
|
|
elif int(cluster_bbox[3]) < int(bbox[1]):
|
||
|
|
dy = int(bbox[1]) - int(cluster_bbox[3])
|
||
|
|
distance = dx + dy
|
||
|
|
if nearest_media_distance is None or distance < nearest_media_distance:
|
||
|
|
nearest_media_distance = distance
|
||
|
|
nearest_media = cluster_bbox
|
||
|
|
del center_inside_y
|
||
|
|
nearest_caption_above = None
|
||
|
|
nearest_caption_below = None
|
||
|
|
for caption in caption_blocks:
|
||
|
|
cb = [int(value) for value in caption.get("block_bbox", [0, 0, 0, 0])]
|
||
|
|
if cb[3] <= bbox[1] and (nearest_caption_above is None or cb[3] > nearest_caption_above[3]):
|
||
|
|
nearest_caption_above = cb
|
||
|
|
if cb[1] >= bbox[3] and (nearest_caption_below is None or cb[1] < nearest_caption_below[1]):
|
||
|
|
nearest_caption_below = cb
|
||
|
|
media_between_block_and_caption = 0
|
||
|
|
media_x_covering_block = 0
|
||
|
|
precaption_region = None
|
||
|
|
if nearest_caption_below:
|
||
|
|
precaption_region = _precaption_media_region(nearest_caption_below, cluster_bboxes)
|
||
|
|
for cluster_bbox in cluster_bboxes:
|
||
|
|
if cluster_bbox[1] >= nearest_caption_below[1] + 24:
|
||
|
|
continue
|
||
|
|
if cluster_bbox[3] <= bbox[1] - 24:
|
||
|
|
continue
|
||
|
|
media_between_block_and_caption += 1
|
||
|
|
if (
|
||
|
|
cluster_bbox[0] - 24 <= _bbox_center_x(bbox) <= cluster_bbox[2] + 24
|
||
|
|
or _bbox_horizontal_overlap_ratio(bbox, cluster_bbox) >= 0.35
|
||
|
|
):
|
||
|
|
media_x_covering_block += 1
|
||
|
|
if nearest_caption_above:
|
||
|
|
caption_gap = bbox[1] - nearest_caption_above[3]
|
||
|
|
left_align_gap = abs(bbox[0] - nearest_caption_above[0])
|
||
|
|
effective_page_width = max(page_width, width)
|
||
|
|
if (
|
||
|
|
0 <= caption_gap <= 72
|
||
|
|
and left_align_gap <= 80
|
||
|
|
and (width >= int(effective_page_width * 0.45))
|
||
|
|
and (not stacked_media_above)
|
||
|
|
):
|
||
|
|
return False
|
||
|
|
if is_body_paragraph_like_text_block(
|
||
|
|
block,
|
||
|
|
body_column_width=body_column_width,
|
||
|
|
cluster_bboxes=cluster_bboxes,
|
||
|
|
caption_bbox=nearest_caption_below,
|
||
|
|
):
|
||
|
|
return False
|
||
|
|
score = 0.0
|
||
|
|
if is_subfigure_label(text):
|
||
|
|
score += 4.0
|
||
|
|
if width <= int(max(body_column_width, 1) * 0.78):
|
||
|
|
score += 1.4
|
||
|
|
elif width <= int(max(body_column_width, 1) * 0.9):
|
||
|
|
score += 0.5
|
||
|
|
if label == "paragraph_title" and len(text) <= 24:
|
||
|
|
score += 0.8
|
||
|
|
if height <= 26:
|
||
|
|
score += 0.8
|
||
|
|
elif height <= 34:
|
||
|
|
score += 0.4
|
||
|
|
if page_width and width <= int(page_width * 0.22):
|
||
|
|
score += 0.5
|
||
|
|
if close_media_count >= 2:
|
||
|
|
score += 2.0
|
||
|
|
elif close_media_count == 1:
|
||
|
|
score += 1.1
|
||
|
|
if stacked_media_above and stacked_media_below:
|
||
|
|
score += 2.2
|
||
|
|
elif stacked_media_above or stacked_media_below:
|
||
|
|
score += 0.9
|
||
|
|
if side_media:
|
||
|
|
score += 1.0
|
||
|
|
if nearest_caption_below and nearest_media:
|
||
|
|
caption_gap = nearest_caption_below[1] - bbox[3]
|
||
|
|
media_gap = min(
|
||
|
|
abs(bbox[1] - nearest_media[3]),
|
||
|
|
abs(nearest_media[1] - bbox[3]),
|
||
|
|
abs(_bbox_center_y(bbox) - _bbox_center_y(nearest_media)),
|
||
|
|
)
|
||
|
|
if 0 <= caption_gap <= 120 and media_gap <= 80:
|
||
|
|
score += 0.8
|
||
|
|
if nearest_caption_below:
|
||
|
|
caption_gap = nearest_caption_below[1] - bbox[3]
|
||
|
|
if 0 <= caption_gap <= 520 and media_between_block_and_caption >= 3:
|
||
|
|
score += 0.9
|
||
|
|
if 0 <= caption_gap <= 520 and media_x_covering_block >= 2:
|
||
|
|
score += 1.3
|
||
|
|
if (
|
||
|
|
0 <= caption_gap <= 520
|
||
|
|
and media_x_covering_block >= 1
|
||
|
|
and (width <= int(max(page_width, width) * 0.82))
|
||
|
|
and (height <= 96)
|
||
|
|
):
|
||
|
|
score += 0.8
|
||
|
|
if precaption_region:
|
||
|
|
region_overlap = _bbox_horizontal_overlap_ratio(bbox, precaption_region)
|
||
|
|
within_region_y = (
|
||
|
|
int(precaption_region[1]) - 24 <= int(bbox[1]) <= int(precaption_region[3]) + 24
|
||
|
|
and int(bbox[3]) <= int(nearest_caption_below[1]) + 24
|
||
|
|
)
|
||
|
|
if region_overlap >= 0.55 and within_region_y:
|
||
|
|
score += 1.5
|
||
|
|
if (
|
||
|
|
int(precaption_region[0]) - 24 <= _bbox_center_x(bbox) <= int(precaption_region[2]) + 24
|
||
|
|
and within_region_y
|
||
|
|
and (media_between_block_and_caption >= 2)
|
||
|
|
):
|
||
|
|
score += 1.1
|
||
|
|
if len(text) <= 22:
|
||
|
|
score += 0.15
|
||
|
|
return score >= 2.6
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def is_formal_figure_legend ===
|
||
|
|
def is_formal_figure_legend(text: str) -> bool:
|
||
|
|
cleaned = clean_block_text(text)
|
||
|
|
if not cleaned:
|
||
|
|
return False
|
||
|
|
return bool(
|
||
|
|
re.match(
|
||
|
|
"^(?:Extended\\s+Data\\s+Fig\\.?\\s+\\w+|Extended\\s+Data\\s+Figure\\s+\\w+|Extended\\s+Data\\s+Table\\s+\\w+|Supplementary\\s+Fig\\.?\\s+\\w+|Supplementary\\s+Figure\\s+\\w+|Supplementary\\s+Table\\s+\\w+|Supplementary\\s+Video\\s+\\w+|Figure\\s+\\d+|Fig\\.?\\s+\\d+|Table\\s+\\d+|Scheme\\s+\\w+|Graphical\\s+Abstract(?:\\s*[:|.\\-].*)?)",
|
||
|
|
cleaned,
|
||
|
|
flags=re.IGNORECASE,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def is_numbered_figure_caption ===
|
||
|
|
def is_numbered_figure_caption(text: str) -> bool:
|
||
|
|
cleaned = clean_block_text(text)
|
||
|
|
if not re.match(r"^(?:Figure|Fig\.?)\s+\d+\b", cleaned, flags=re.IGNORECASE):
|
||
|
|
return False
|
||
|
|
return not re.match(
|
||
|
|
r"^(?:Figure|Fig\.?)\s+\d+\s+(?:shows?|illustrates?|depicts?|describes?|summarizes?)\b",
|
||
|
|
cleaned,
|
||
|
|
flags=re.IGNORECASE,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def is_subfigure_label ===
|
||
|
|
def is_subfigure_label(text: str) -> bool:
|
||
|
|
compact = re.sub("\\s+", " ", text.strip().lower())
|
||
|
|
return bool(
|
||
|
|
re.fullmatch("(?:\\([a-z]\\)\\s*)+", compact)
|
||
|
|
or re.fullmatch("[a-z]", compact)
|
||
|
|
or re.fullmatch("[a-z]\\)", compact)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
=== def media_clusters ===
|
||
|
|
def media_clusters(blocks: list[dict]) -> tuple[dict[int, int], list[list[dict]]]:
|
||
|
|
media = [b for b in blocks if b.get("block_label") in {"image", "chart"}]
|
||
|
|
clusters: list[list[dict]] = []
|
||
|
|
block_to_cluster: dict[int, int] = {}
|
||
|
|
for block in media:
|
||
|
|
x1, y1, x2, y2 = block.get("block_bbox", [0, 0, 0, 0])
|
||
|
|
assigned = None
|
||
|
|
for idx, cluster in enumerate(clusters):
|
||
|
|
cx1 = min(item["block_bbox"][0] for item in cluster) - 40
|
||
|
|
cy1 = min(item["block_bbox"][1] for item in cluster) - 40
|
||
|
|
cx2 = max(item["block_bbox"][2] for item in cluster) + 40
|
||
|
|
cy2 = max(item["block_bbox"][3] for item in cluster) + 40
|
||
|
|
vertical_overlap = max(0, min(y2, cy2) - max(y1, cy1))
|
||
|
|
min_height = max(1, min(y2 - y1, cy2 - cy1))
|
||
|
|
if x2 < cx1:
|
||
|
|
horizontal_gap = cx1 - x2
|
||
|
|
elif x1 > cx2:
|
||
|
|
horizontal_gap = x1 - cx2
|
||
|
|
else:
|
||
|
|
horizontal_gap = 0
|
||
|
|
overlaps_cluster = not (x2 < cx1 or x1 > cx2 or y2 < cy1 or y1 > cy2)
|
||
|
|
side_by_side_panel = vertical_overlap >= int(min_height * 0.35) and horizontal_gap <= 60
|
||
|
|
if overlaps_cluster or side_by_side_panel:
|
||
|
|
assigned = idx
|
||
|
|
break
|
||
|
|
if assigned is None:
|
||
|
|
assigned = len(clusters)
|
||
|
|
clusters.append([])
|
||
|
|
clusters[assigned].append(block)
|
||
|
|
block_to_cluster[block.get("block_id", -1)] = assigned
|
||
|
|
return (block_to_cluster, clusters)
|
||
|
|
|
||
|
|
|
||
|
|
|