"""Grounding-point helpers (point / bbox coordinate remap, pure regex).""" import re from typing import List, Optional def process_grounding_points( text: str, orig_height, orig_width, resized_height, resized_width, model_type ) -> str: """Remap // coordinates inside ``text`` from the original image size to the resized space used by the given model type. """ point_pattern = re.compile(r"<(point|box|bbox)>(.*?)") def process_match(match): tag_name = match.group(1) coords_str = match.group(2) try: coords = list(map(int, re.findall(r"\d+", coords_str))) scale_w = resized_width / orig_width scale_h = resized_height / orig_height if len(coords) == 2: x, y = coords if model_type == "qwen2_5": new_x = max(0, min(round(x * scale_w), resized_width - 1)) new_y = max(0, min(round(y * scale_h), resized_height - 1)) elif model_type in ["qwen2"]: new_x = max(0, min(999.999, (x / orig_width) * 1000)) new_y = max(0, min(999.999, (y / orig_height) * 1000)) else: raise ValueError("Unsupported model type") coords = [new_x, new_y] if len(coords) == 4: x1, y1, x2, y2 = coords if model_type == "qwen2_5": new_x1 = max(0, min(round(x1 * scale_w), resized_width - 1)) new_y1 = max(0, min(round(y1 * scale_h), resized_height - 1)) new_x2 = max(0, min(round(x2 * scale_w), resized_width - 1)) new_y2 = max(0, min(round(y2 * scale_h), resized_height - 1)) elif model_type in ["qwen2"]: new_x1 = max(0, min(999.999, (x1 / orig_width) * 1000)) new_y1 = max(0, min(999.999, (y1 / orig_height) * 1000)) new_x2 = max(0, min(999.999, (x2 / orig_width) * 1000)) new_y2 = max(0, min(999.999, (y2 / orig_height) * 1000)) else: raise ValueError("Unsupported model type") coords = [new_x1, new_y1, new_x2, new_y2] return f'<{tag_name}>[{", ".join(map(str, coords))}]' except (ValueError, TypeError): return match.group(0) return point_pattern.sub(process_match, text) def extract_grounding_points(text: str) -> List[List[float]]: """Extract all // coordinates from ``text`` as list-of-list.""" point_pattern = re.compile(r"<(point|box|bbox)>\s*\[([^\]]+)\]\s*") points: List[List[float]] = [] for match in point_pattern.finditer(text): coords_str = match.group(2) raw_values = re.findall(r"-?\d+\.?\d*", coords_str) converted: List[float] = [] for value in raw_values: number = float(value) converted.append(int(number) if number.is_integer() else number) if converted: points.append(converted) return points def reverse_grounding_points( text: str, orig_height, orig_width, resized_height, resized_width, model_type ) -> str: """Inverse of ``process_grounding_points`` - map resized coords back to original.""" point_pattern = re.compile(r"<(point|box|bbox)>(.*?)") def reverse_match(match): tag_name = match.group(1) coords_str = match.group(2) try: coords = list(map(float, re.findall(r"-?\d+\.?\d*", coords_str))) scale_w = resized_width / orig_width scale_h = resized_height / orig_height if len(coords) == 2: x, y = coords if model_type == "qwen2_5": orig_x = max(0, min(orig_width - 1, round(x / scale_w))) orig_y = max(0, min(orig_height - 1, round(y / scale_h))) elif model_type in ["qwen2"]: orig_x = max( 0, min(orig_width - 1, round((x / 1000) * orig_width)) ) orig_y = max( 0, min(orig_height - 1, round((y / 1000) * orig_height)) ) else: raise ValueError("Unsupported model type") coords = [orig_x, orig_y] if len(coords) == 4: x1, y1, x2, y2 = coords if model_type == "qwen2_5": orig_x1 = max(0, min(orig_width - 1, round(x1 / scale_w))) orig_y1 = max(0, min(orig_height - 1, round(y1 / scale_h))) orig_x2 = max(0, min(orig_width - 1, round(x2 / scale_w))) orig_y2 = max(0, min(orig_height - 1, round(y2 / scale_h))) elif model_type in ["qwen2"]: orig_x1 = max( 0, min(orig_width - 1, round((x1 / 1000) * orig_width)) ) orig_y1 = max( 0, min(orig_height - 1, round((y1 / 1000) * orig_height)) ) orig_x2 = max( 0, min(orig_width - 1, round((x2 / 1000) * orig_width)) ) orig_y2 = max( 0, min(orig_height - 1, round((y2 / 1000) * orig_height)) ) else: raise ValueError("Unsupported model type") coords = [orig_x1, orig_y1, orig_x2, orig_y2] return f'<{tag_name}>[{", ".join(map(str, map(int, coords)))}]' except (ValueError, TypeError): return match.group(0) return point_pattern.sub(reverse_match, text) def calculate_point_l1_distance(gt_text: str, pred_text: str) -> Optional[float]: """Average L1 distance between 2D points extracted from ``gt_text`` / ``pred_text``. Returns None if either side has no points or counts differ. """ point_pattern = re.compile(r"\[(\d+),\s*(\d+)\]") gt_matches = point_pattern.findall(gt_text) pred_matches = point_pattern.findall(pred_text) if not gt_matches or not pred_matches or len(gt_matches) != len(pred_matches): return None total_l1_distance = 0.0 for (gt_x, gt_y), (pred_x, pred_y) in zip(gt_matches, pred_matches): try: gt_x, gt_y = int(gt_x), int(gt_y) pred_x, pred_y = int(pred_x), int(pred_y) l1_dist = abs(gt_x - pred_x) + abs(gt_y - pred_y) total_l1_distance += l1_dist except ValueError: continue return total_l1_distance / len(gt_matches) if gt_matches else None __all__ = [ "process_grounding_points", "extract_grounding_points", "reverse_grounding_points", "calculate_point_l1_distance", ]