168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
import numpy as np
|
|
|
|
class Box:
|
|
def __init__(self, region):
|
|
self.id = region['id']
|
|
self.type = region['type']
|
|
self.bbox = region['bbox']
|
|
self.x1, self.y1, self.x2, self.y2 = self.bbox
|
|
self.region = region
|
|
|
|
def recursive_xy_cut(boxes, min_x_gap=20, min_y_gap=20):
|
|
if not boxes:
|
|
return []
|
|
|
|
if len(boxes) == 1:
|
|
return [boxes]
|
|
|
|
# Find bounding box of all boxes
|
|
min_x = min(b.x1 for b in boxes)
|
|
max_x = max(b.x2 for b in boxes)
|
|
min_y = min(b.y1 for b in boxes)
|
|
max_y = max(b.y2 for b in boxes)
|
|
|
|
# Projection profiles
|
|
x_profile = np.zeros(max_x - min_x + 1, dtype=int)
|
|
y_profile = np.zeros(max_y - min_y + 1, dtype=int)
|
|
|
|
for b in boxes:
|
|
x_profile[max(0, b.x1 - min_x):b.x2 - min_x + 1] = 1
|
|
y_profile[max(0, b.y1 - min_y):b.y2 - min_y + 1] = 1
|
|
|
|
# Find gaps
|
|
def find_gaps(profile):
|
|
gaps = []
|
|
in_gap = profile[0] == 0
|
|
start = 0 if in_gap else -1
|
|
for i, val in enumerate(profile):
|
|
if val == 0 and not in_gap:
|
|
in_gap = True
|
|
start = i
|
|
elif val == 1 and in_gap:
|
|
in_gap = False
|
|
gaps.append((start, i))
|
|
if in_gap:
|
|
gaps.append((start, len(profile)))
|
|
return gaps
|
|
|
|
x_gaps = find_gaps(x_profile)
|
|
y_gaps = find_gaps(y_profile)
|
|
|
|
# Filter gaps by threshold
|
|
valid_x_gaps = [g for g in x_gaps if g[1] - g[0] >= min_x_gap]
|
|
valid_y_gaps = [g for g in y_gaps if g[1] - g[0] >= min_y_gap]
|
|
|
|
# Choose best cut (prefer Y cuts to separate vertical sections, then X cuts for columns)
|
|
# Actually, in newspapers, Y cut (horizontal line) separates main stories or headlines from columns
|
|
# X cut (vertical line) separates columns.
|
|
# Standard XY cut alternates or picks the widest gap.
|
|
best_cut = None
|
|
axis = None
|
|
|
|
if valid_y_gaps:
|
|
# Pick the widest Y gap
|
|
widest = max(valid_y_gaps, key=lambda g: g[1] - g[0])
|
|
best_cut = widest[0] + (widest[1] - widest[0]) // 2 + min_y
|
|
axis = 'y'
|
|
elif valid_x_gaps:
|
|
# Pick the widest X gap
|
|
widest = max(valid_x_gaps, key=lambda g: g[1] - g[0])
|
|
best_cut = widest[0] + (widest[1] - widest[0]) // 2 + min_x
|
|
axis = 'x'
|
|
|
|
if best_cut is None:
|
|
# No cuts can be made, this is a leaf node
|
|
return [boxes]
|
|
|
|
# Split boxes
|
|
left_top = []
|
|
right_bottom = []
|
|
|
|
if axis == 'y':
|
|
for b in boxes:
|
|
if b.y1 + (b.y2 - b.y1)//2 < best_cut:
|
|
left_top.append(b)
|
|
else:
|
|
right_bottom.append(b)
|
|
else:
|
|
for b in boxes:
|
|
if b.x1 + (b.x2 - b.x1)//2 < best_cut:
|
|
left_top.append(b)
|
|
else:
|
|
right_bottom.append(b)
|
|
|
|
# Recurse
|
|
# If a cut didn't actually split anything (due to bounding box overlap centers), stop
|
|
if not left_top or not right_bottom:
|
|
return [boxes]
|
|
|
|
return recursive_xy_cut(left_top, min_x_gap, min_y_gap) + recursive_xy_cut(right_bottom, min_x_gap, min_y_gap)
|
|
|
|
def group_surya_regions_xycut(regions):
|
|
"""
|
|
Groups regions by using XY-Cut to establish reading order,
|
|
then grouping by paragraph_title / doc_title.
|
|
"""
|
|
if not regions:
|
|
return []
|
|
|
|
boxes = [Box(r) for r in regions]
|
|
|
|
# Step 1: XY-Cut
|
|
leaves = recursive_xy_cut(boxes, min_x_gap=30, min_y_gap=25)
|
|
|
|
# Step 2: Establish Reading order
|
|
# Leaves are already in a roughly top-to-bottom, left-to-right order because
|
|
# recursive_xy_cut processes Y cuts then X cuts and appends left_top before right_bottom.
|
|
|
|
# Flatten boxes in reading order
|
|
ordered_boxes = []
|
|
for leaf in leaves:
|
|
# Sort within leaf just in case
|
|
leaf.sort(key=lambda b: (b.y1, b.x1))
|
|
ordered_boxes.extend(leaf)
|
|
|
|
# Step 3: Group into articles
|
|
articles = []
|
|
current_article = []
|
|
current_title = None
|
|
|
|
for box in ordered_boxes:
|
|
# Start a new article if we hit a title
|
|
if box.type in ('paragraph_title', 'doc_title'):
|
|
# If the current article ONLY contains images, these images likely belong to this new title!
|
|
# Or if it contains images at the very end, wait, let's just check if it's ONLY images.
|
|
if current_article and all(b.type == 'image' for b in current_article):
|
|
current_article.append(box)
|
|
else:
|
|
if current_article:
|
|
articles.append(current_article)
|
|
current_article = [box]
|
|
else:
|
|
current_article.append(box)
|
|
|
|
if current_article:
|
|
articles.append(current_article)
|
|
|
|
# Format output
|
|
output = []
|
|
for i, art_boxes in enumerate(articles, start=1):
|
|
member_ids = [b.id for b in art_boxes]
|
|
|
|
# Calculate union bbox
|
|
min_x = min(b.x1 for b in art_boxes)
|
|
min_y = min(b.y1 for b in art_boxes)
|
|
max_x = max(b.x2 for b in art_boxes)
|
|
max_y = max(b.y2 for b in art_boxes)
|
|
|
|
output.append({
|
|
"article_id": i,
|
|
"title_region": member_ids[0] if art_boxes[0].type in ('paragraph_title', 'doc_title') else None,
|
|
"member_regions": member_ids,
|
|
"bbox": [min_x, min_y, max_x, max_y],
|
|
"dateline": None,
|
|
"grouped_by": "xy_cut"
|
|
})
|
|
|
|
return output
|