amg.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
  2. import math
  3. from itertools import product
  4. from typing import Any, Generator, List, Tuple
  5. import numpy as np
  6. import torch
  7. def is_box_near_crop_edge(
  8. boxes: torch.Tensor, crop_box: List[int], orig_box: List[int], atol: float = 20.0
  9. ) -> torch.Tensor:
  10. """Determines if bounding boxes are near the edge of a cropped image region using a specified tolerance."""
  11. crop_box_torch = torch.as_tensor(crop_box, dtype=torch.float, device=boxes.device)
  12. orig_box_torch = torch.as_tensor(orig_box, dtype=torch.float, device=boxes.device)
  13. boxes = uncrop_boxes_xyxy(boxes, crop_box).float()
  14. near_crop_edge = torch.isclose(boxes, crop_box_torch[None, :], atol=atol, rtol=0)
  15. near_image_edge = torch.isclose(boxes, orig_box_torch[None, :], atol=atol, rtol=0)
  16. near_crop_edge = torch.logical_and(near_crop_edge, ~near_image_edge)
  17. return torch.any(near_crop_edge, dim=1)
  18. def batch_iterator(batch_size: int, *args) -> Generator[List[Any], None, None]:
  19. """Yields batches of data from input arguments with specified batch size for efficient processing."""
  20. assert args and all(len(a) == len(args[0]) for a in args), "Batched iteration must have same-size inputs."
  21. n_batches = len(args[0]) // batch_size + int(len(args[0]) % batch_size != 0)
  22. for b in range(n_batches):
  23. yield [arg[b * batch_size : (b + 1) * batch_size] for arg in args]
  24. def calculate_stability_score(masks: torch.Tensor, mask_threshold: float, threshold_offset: float) -> torch.Tensor:
  25. """
  26. Computes the stability score for a batch of masks.
  27. The stability score is the IoU between binary masks obtained by thresholding the predicted mask logits at
  28. high and low values.
  29. Args:
  30. masks (torch.Tensor): Batch of predicted mask logits.
  31. mask_threshold (float): Threshold value for creating binary masks.
  32. threshold_offset (float): Offset applied to the threshold for creating high and low binary masks.
  33. Returns:
  34. (torch.Tensor): Stability scores for each mask in the batch.
  35. Notes:
  36. - One mask is always contained inside the other.
  37. - Memory is saved by preventing unnecessary cast to torch.int64.
  38. Examples:
  39. >>> masks = torch.rand(10, 256, 256) # Batch of 10 masks
  40. >>> mask_threshold = 0.5
  41. >>> threshold_offset = 0.1
  42. >>> stability_scores = calculate_stability_score(masks, mask_threshold, threshold_offset)
  43. """
  44. intersections = (masks > (mask_threshold + threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32)
  45. unions = (masks > (mask_threshold - threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32)
  46. return intersections / unions
  47. def build_point_grid(n_per_side: int) -> np.ndarray:
  48. """Generate a 2D grid of evenly spaced points in the range [0,1]x[0,1] for image segmentation tasks."""
  49. offset = 1 / (2 * n_per_side)
  50. points_one_side = np.linspace(offset, 1 - offset, n_per_side)
  51. points_x = np.tile(points_one_side[None, :], (n_per_side, 1))
  52. points_y = np.tile(points_one_side[:, None], (1, n_per_side))
  53. return np.stack([points_x, points_y], axis=-1).reshape(-1, 2)
  54. def build_all_layer_point_grids(n_per_side: int, n_layers: int, scale_per_layer: int) -> List[np.ndarray]:
  55. """Generates point grids for multiple crop layers with varying scales and densities."""
  56. return [build_point_grid(int(n_per_side / (scale_per_layer**i))) for i in range(n_layers + 1)]
  57. def generate_crop_boxes(
  58. im_size: Tuple[int, ...], n_layers: int, overlap_ratio: float
  59. ) -> Tuple[List[List[int]], List[int]]:
  60. """Generates crop boxes of varying sizes for multiscale image processing, with layered overlapping regions."""
  61. crop_boxes, layer_idxs = [], []
  62. im_h, im_w = im_size
  63. short_side = min(im_h, im_w)
  64. # Original image
  65. crop_boxes.append([0, 0, im_w, im_h])
  66. layer_idxs.append(0)
  67. def crop_len(orig_len, n_crops, overlap):
  68. """Crops bounding boxes to the size of the input image."""
  69. return int(math.ceil((overlap * (n_crops - 1) + orig_len) / n_crops))
  70. for i_layer in range(n_layers):
  71. n_crops_per_side = 2 ** (i_layer + 1)
  72. overlap = int(overlap_ratio * short_side * (2 / n_crops_per_side))
  73. crop_w = crop_len(im_w, n_crops_per_side, overlap)
  74. crop_h = crop_len(im_h, n_crops_per_side, overlap)
  75. crop_box_x0 = [int((crop_w - overlap) * i) for i in range(n_crops_per_side)]
  76. crop_box_y0 = [int((crop_h - overlap) * i) for i in range(n_crops_per_side)]
  77. # Crops in XYWH format
  78. for x0, y0 in product(crop_box_x0, crop_box_y0):
  79. box = [x0, y0, min(x0 + crop_w, im_w), min(y0 + crop_h, im_h)]
  80. crop_boxes.append(box)
  81. layer_idxs.append(i_layer + 1)
  82. return crop_boxes, layer_idxs
  83. def uncrop_boxes_xyxy(boxes: torch.Tensor, crop_box: List[int]) -> torch.Tensor:
  84. """Uncrop bounding boxes by adding the crop box offset to their coordinates."""
  85. x0, y0, _, _ = crop_box
  86. offset = torch.tensor([[x0, y0, x0, y0]], device=boxes.device)
  87. # Check if boxes has a channel dimension
  88. if len(boxes.shape) == 3:
  89. offset = offset.unsqueeze(1)
  90. return boxes + offset
  91. def uncrop_points(points: torch.Tensor, crop_box: List[int]) -> torch.Tensor:
  92. """Uncrop points by adding the crop box offset to their coordinates."""
  93. x0, y0, _, _ = crop_box
  94. offset = torch.tensor([[x0, y0]], device=points.device)
  95. # Check if points has a channel dimension
  96. if len(points.shape) == 3:
  97. offset = offset.unsqueeze(1)
  98. return points + offset
  99. def uncrop_masks(masks: torch.Tensor, crop_box: List[int], orig_h: int, orig_w: int) -> torch.Tensor:
  100. """Uncrop masks by padding them to the original image size, handling coordinate transformations."""
  101. x0, y0, x1, y1 = crop_box
  102. if x0 == 0 and y0 == 0 and x1 == orig_w and y1 == orig_h:
  103. return masks
  104. # Coordinate transform masks
  105. pad_x, pad_y = orig_w - (x1 - x0), orig_h - (y1 - y0)
  106. pad = (x0, pad_x - x0, y0, pad_y - y0)
  107. return torch.nn.functional.pad(masks, pad, value=0)
  108. def remove_small_regions(mask: np.ndarray, area_thresh: float, mode: str) -> Tuple[np.ndarray, bool]:
  109. """Removes small disconnected regions or holes in a mask based on area threshold and mode."""
  110. import cv2 # type: ignore
  111. assert mode in {"holes", "islands"}, f"Provided mode {mode} is invalid"
  112. correct_holes = mode == "holes"
  113. working_mask = (correct_holes ^ mask).astype(np.uint8)
  114. n_labels, regions, stats, _ = cv2.connectedComponentsWithStats(working_mask, 8)
  115. sizes = stats[:, -1][1:] # Row 0 is background label
  116. small_regions = [i + 1 for i, s in enumerate(sizes) if s < area_thresh]
  117. if not small_regions:
  118. return mask, False
  119. fill_labels = [0] + small_regions
  120. if not correct_holes:
  121. # If every region is below threshold, keep largest
  122. fill_labels = [i for i in range(n_labels) if i not in fill_labels] or [int(np.argmax(sizes)) + 1]
  123. mask = np.isin(regions, fill_labels)
  124. return mask, True
  125. def batched_mask_to_box(masks: torch.Tensor) -> torch.Tensor:
  126. """Calculates bounding boxes in XYXY format around binary masks, handling empty masks and various input shapes."""
  127. # torch.max below raises an error on empty inputs, just skip in this case
  128. if torch.numel(masks) == 0:
  129. return torch.zeros(*masks.shape[:-2], 4, device=masks.device)
  130. # Normalize shape to CxHxW
  131. shape = masks.shape
  132. h, w = shape[-2:]
  133. masks = masks.flatten(0, -3) if len(shape) > 2 else masks.unsqueeze(0)
  134. # Get top and bottom edges
  135. in_height, _ = torch.max(masks, dim=-1)
  136. in_height_coords = in_height * torch.arange(h, device=in_height.device)[None, :]
  137. bottom_edges, _ = torch.max(in_height_coords, dim=-1)
  138. in_height_coords = in_height_coords + h * (~in_height)
  139. top_edges, _ = torch.min(in_height_coords, dim=-1)
  140. # Get left and right edges
  141. in_width, _ = torch.max(masks, dim=-2)
  142. in_width_coords = in_width * torch.arange(w, device=in_width.device)[None, :]
  143. right_edges, _ = torch.max(in_width_coords, dim=-1)
  144. in_width_coords = in_width_coords + w * (~in_width)
  145. left_edges, _ = torch.min(in_width_coords, dim=-1)
  146. # If the mask is empty the right edge will be to the left of the left edge.
  147. # Replace these boxes with [0, 0, 0, 0]
  148. empty_filter = (right_edges < left_edges) | (bottom_edges < top_edges)
  149. out = torch.stack([left_edges, top_edges, right_edges, bottom_edges], dim=-1)
  150. out = out * (~empty_filter).unsqueeze(-1)
  151. # Return to original shape
  152. return out.reshape(*shape[:-2], 4) if len(shape) > 2 else out[0]