import numpy as np def merge_line_boxes(det_res, x_margin_ratio=2.5, y_overlap_thresh=0.45): """ Умное объединение разрезанных боксов одной строки (Horizontal Line Merging). :param det_res: список кортежей (box, text, score) от PaddleOCR/RapidOCR :param x_margin_ratio: максимальное расстояние между словами одной строки (в долях высоты) :param y_overlap_thresh: минимальный коэффициент вертикального перекрытия для принадлежности к одной строке """ if det_res is None or len(det_res) == 0: return [] # Преобразуем в структурированные прямоугольники [xmin, ymin, xmax, ymax] rects = [] for item in det_res: if isinstance(item, (list, tuple)) and len(item) > 0 and isinstance(item[0], (list, tuple, np.ndarray)): box = np.array(item[0]).astype(int) else: box = np.array(item).astype(int) if box.ndim == 2 and box.shape[0] >= 4: xmin = int(np.min(box[:, 0])) xmax = int(np.max(box[:, 0])) ymin = int(np.min(box[:, 1])) ymax = int(np.max(box[:, 1])) elif len(box) == 4: xmin, ymin, xmax, ymax = int(box[0]), int(box[1]), int(box[2]), int(box[3]) else: continue h = max(1, ymax - ymin) w = max(1, xmax - xmin) rects.append({ "box": [xmin, ymin, xmax, ymax], "xmin": xmin, "xmax": xmax, "ymin": ymin, "ymax": ymax, "h": h, "w": w, "cy": (ymin + ymax) / 2.0 }) if not rects: return [] # Сортируем сначала по вертикали Y rects.sort(key=lambda r: (r["ymin"], r["xmin"])) # Группируем боксы по строкам с проверкой близости вертикальных центров lines = [] for r in rects: placed = False for line in lines: line_ymin = min(x["ymin"] for x in line) line_ymax = max(x["ymax"] for x in line) line_cy = (line_ymin + line_ymax) / 2.0 line_h = max(1, line_ymax - line_ymin) # Проверяем расстояние между центрами строк center_diff = abs(r["cy"] - line_cy) if center_diff <= min(r["h"], line_h) * 0.45: line.append(r) placed = True break if not placed: lines.append([r]) # Внутри каждой строки сортируем слева направо и объединяем merged_boxes = [] for line in lines: line.sort(key=lambda r: r["xmin"]) cur_xmin = line[0]["xmin"] cur_xmax = line[0]["xmax"] cur_ymin = line[0]["ymin"] cur_ymax = line[0]["ymax"] for next_r in line[1:]: avg_h = ((cur_ymax - cur_ymin) + next_r["h"]) / 2.0 dist_x = next_r["xmin"] - cur_xmax # Если боксы достаточно близко по горизонтали — объединяем в один if dist_x <= avg_h * x_margin_ratio: cur_xmax = max(cur_xmax, next_r["xmax"]) cur_ymin = min(cur_ymin, next_r["ymin"]) cur_ymax = max(cur_ymax, next_r["ymax"]) else: # Слишком далеко (например, две колонки текста) — сохраняем отдельно merged_boxes.append([cur_xmin, cur_ymin, cur_xmax, cur_ymax]) cur_xmin = next_r["xmin"] cur_xmax = next_r["xmax"] cur_ymin = next_r["ymin"] cur_ymax = next_r["ymax"] merged_boxes.append([cur_xmin, cur_ymin, cur_xmax, cur_ymax]) # Сортируем итоговые строки строго сверху вниз merged_boxes.sort(key=lambda b: (b[1], b[0])) return merged_boxes