| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- """
- 通用坐标转换工具模块
- 提供通用的坐标计算和转换功能:
- - 底层坐标计算(IoU、重叠比例)
- - 多边形/bbox 格式转换
- - 相对坐标 → 绝对坐标转换
- - 图像裁剪
- - 格式检测
- 此模块从 universal_doc_parser 中提取,供多个模块共享使用。
- """
- from typing import List, Tuple, Union, Any
- import numpy as np
- # 导入 MinerU 组件(用于 IoU 计算优化)
- try:
- from mineru.utils.boxbase import calculate_iou as mineru_calculate_iou
- from mineru.utils.boxbase import calculate_overlap_area_2_minbox_area_ratio
- MINERU_BOXBASE_AVAILABLE = True
- except ImportError:
- MINERU_BOXBASE_AVAILABLE = False
- mineru_calculate_iou = None
- calculate_overlap_area_2_minbox_area_ratio = None
- class CoordinateUtils:
- """通用坐标转换工具类"""
-
- # ==================== 底层坐标计算方法 ====================
-
- @staticmethod
- def calculate_iou(bbox1: List[float], bbox2: List[float]) -> float:
- """
- 计算两个 bbox 的 IoU(交并比)
-
- Args:
- bbox1: 第一个 bbox [x1, y1, x2, y2]
- bbox2: 第二个 bbox [x1, y1, x2, y2]
-
- Returns:
- IoU 值
- """
- if MINERU_BOXBASE_AVAILABLE and mineru_calculate_iou is not None:
- return mineru_calculate_iou(bbox1, bbox2)
-
- # 备用实现
- x_left = max(bbox1[0], bbox2[0])
- y_top = max(bbox1[1], bbox2[1])
- x_right = min(bbox1[2], bbox2[2])
- y_bottom = min(bbox1[3], bbox2[3])
-
- if x_right < x_left or y_bottom < y_top:
- return 0.0
-
- intersection_area = (x_right - x_left) * (y_bottom - y_top)
- bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
- bbox2_area = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
-
- if bbox1_area == 0 or bbox2_area == 0:
- return 0.0
-
- return intersection_area / float(bbox1_area + bbox2_area - intersection_area)
-
- @staticmethod
- def calculate_overlap_ratio(bbox1: List[float], bbox2: List[float]) -> float:
- """
- 计算重叠面积占小框面积的比例
-
- Args:
- bbox1: 第一个 bbox [x1, y1, x2, y2]
- bbox2: 第二个 bbox [x1, y1, x2, y2]
-
- Returns:
- 重叠比例
- """
- if MINERU_BOXBASE_AVAILABLE and calculate_overlap_area_2_minbox_area_ratio is not None:
- return calculate_overlap_area_2_minbox_area_ratio(bbox1, bbox2)
-
- # 备用实现
- x_left = max(bbox1[0], bbox2[0])
- y_top = max(bbox1[1], bbox2[1])
- x_right = min(bbox1[2], bbox2[2])
- y_bottom = min(bbox1[3], bbox2[3])
-
- if x_right < x_left or y_bottom < y_top:
- return 0.0
-
- intersection_area = (x_right - x_left) * (y_bottom - y_top)
- area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
- area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
- min_area = min(area1, area2)
-
- if min_area == 0:
- return 0.0
-
- return intersection_area / min_area
-
- @staticmethod
- def calculate_overlap_in_bbox1_ratio(
- bbox1: List[float],
- bbox2: List[float]
- ) -> float:
- """
- 计算 bbox1 被 bbox2 覆盖的面积比例
-
- Args:
- bbox1: 第一个 bbox [x1, y1, x2, y2]
- bbox2: 第二个 bbox [x1, y1, x2, y2]
-
- Returns:
- bbox1 被覆盖的比例
- """
- x_left = max(bbox1[0], bbox2[0])
- y_top = max(bbox1[1], bbox2[1])
- x_right = min(bbox1[2], bbox2[2])
- y_bottom = min(bbox1[3], bbox2[3])
-
- if x_right < x_left or y_bottom < y_top:
- return 0.0
-
- intersection_area = (x_right - x_left) * (y_bottom - y_top)
- bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
-
- if bbox1_area == 0:
- return 0.0
-
- return intersection_area / bbox1_area
-
- @staticmethod
- def poly_to_bbox(poly: Union[List, None]) -> List[float]:
- """
- 将多边形坐标转换为 bbox 格式
-
- Args:
- poly: 多边形坐标,支持以下格式:
- - [[x1,y1], [x2,y1], [x2,y2], [x1,y2]] (4个点)
- - [x1, y1, x2, y1, x2, y2, x1, y2] (8个值)
- - [x1, y1, x2, y2] (4个值,已是bbox)
-
- Returns:
- bbox [x1, y1, x2, y2]
- """
- if not poly:
- return [0, 0, 0, 0]
-
- # 处理嵌套列表格式 [[x1,y1], [x2,y1], ...]
- if isinstance(poly[0], (list, tuple)):
- xs = [p[0] for p in poly]
- ys = [p[1] for p in poly]
- return [min(xs), min(ys), max(xs), max(ys)]
-
- # 处理平面列表格式
- if len(poly) == 4:
- # 已经是 bbox 格式
- return list(poly)
- elif len(poly) >= 8:
- # 8点格式:[x1, y1, x2, y1, x2, y2, x1, y2]
- xs = [poly[i] for i in range(0, len(poly), 2)]
- ys = [poly[i] for i in range(1, len(poly), 2)]
- return [min(xs), min(ys), max(xs), max(ys)]
-
- return [0, 0, 0, 0]
-
- @staticmethod
- def bbox_to_poly(bbox: List[float]) -> List[List[float]]:
- """
- 将 bbox 转换为多边形坐标
-
- Args:
- bbox: [x1, y1, x2, y2]
-
- Returns:
- [[x1,y1], [x2,y1], [x2,y2], [x1,y2]]
- """
- if not bbox or len(bbox) < 4:
- return [[0, 0], [0, 0], [0, 0], [0, 0]]
-
- x1, y1, x2, y2 = bbox[:4]
- return [
- [float(x1), float(y1)],
- [float(x2), float(y1)],
- [float(x2), float(y2)],
- [float(x1), float(y2)]
- ]
-
- # ==================== 图像裁剪 ====================
-
- @staticmethod
- def crop_region(image: np.ndarray, bbox: List[float], padding: int = 0) -> np.ndarray:
- """
- 裁剪图像区域
-
- Args:
- image: 原始图像
- bbox: 裁剪区域 [x1, y1, x2, y2]
- padding: 边缘padding(像素),可以为正数(扩展裁剪区域)或负数(收缩裁剪区域)
-
- Returns:
- 裁剪后的图像
- """
- if len(bbox) < 4:
- return image
-
- h, w = image.shape[:2]
-
- # 解析padding(支持单个值或四个值)
- if isinstance(padding, (int, float)):
- pad_left = pad_right = pad_top = pad_bottom = int(padding)
- else:
- # 假设是长度为4的元组/列表 [left, top, right, bottom]
- if len(padding) >= 4:
- pad_left, pad_top, pad_right, pad_bottom = [int(p) for p in padding[:4]]
- else:
- pad_left = pad_top = pad_right = pad_bottom = 0
- x1 = max(0 - pad_left, int(bbox[0]) - pad_left)
- y1 = max(0 - pad_top, int(bbox[1]) - pad_top)
- x2 = min(w + pad_right, int(bbox[2]) + pad_right)
- y2 = min(h + pad_bottom, int(bbox[3]) + pad_bottom)
- # 确保坐标有效
- x1 = max(0, x1)
- y1 = max(0, y1)
- x2 = min(w, x2)
- y2 = min(h, y2)
- # 检查是否有效区域
- if x2 <= x1 or y2 <= y1:
- return image
-
- return image[y1:y2, x1:x2]
-
- @staticmethod
- def bbox_overlap(bbox1: List[float], bbox2: List[float]) -> bool:
- """
- 检查两个 bbox 是否重叠
-
- Args:
- bbox1: 第一个 bbox [x1, y1, x2, y2]
- bbox2: 第二个 bbox [x1, y1, x2, y2]
-
- Returns:
- 是否重叠
- """
- if len(bbox1) < 4 or len(bbox2) < 4:
- return False
-
- x1_1, y1_1, x2_1, y2_1 = bbox1[:4]
- x1_2, y1_2, x2_2, y2_2 = bbox2[:4]
-
- if x2_1 < x1_2 or x2_2 < x1_1:
- return False
- if y2_1 < y1_2 or y2_2 < y1_1:
- return False
-
- return True
-
- @staticmethod
- def convert_to_absolute_coords(
- relative_bbox: List,
- region_bbox: List[float]
- ) -> List:
- """
- 将相对坐标转换为绝对坐标
-
- Args:
- relative_bbox: 相对坐标
- region_bbox: 区域的绝对坐标 [x1, y1, x2, y2]
-
- Returns:
- 绝对坐标
- """
- if not relative_bbox or len(region_bbox) < 4:
- return relative_bbox
-
- bx1, by1 = region_bbox[0], region_bbox[1]
-
- # 处理4点坐标格式 [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
- if isinstance(relative_bbox[0], (list, tuple)):
- return [
- [p[0] + bx1, p[1] + by1] for p in relative_bbox
- ]
-
- # 处理4值坐标格式 [x1, y1, x2, y2]
- if len(relative_bbox) >= 4:
- return [
- relative_bbox[0] + bx1,
- relative_bbox[1] + by1,
- relative_bbox[2] + bx1,
- relative_bbox[3] + by1
- ]
-
- return relative_bbox
-
- @staticmethod
- def is_poly_format(bbox: Any) -> bool:
- """
- 检测 bbox 是否为四点多边形格式
-
- 四点格式: [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
- 矩形格式: [x_min, y_min, x_max, y_max]
- """
- if not bbox or not isinstance(bbox, list):
- return False
- if len(bbox) != 4:
- return False
- return isinstance(bbox[0], (list, tuple))
|