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