sundeep-news-scan/xy_cut.py

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