yslan/ObjCtrl-2.5D
10
1# Copyright (c) Meta Platforms, Inc. and affiliates.2# All rights reserved.3 4# This source code is licensed under the license found in the5# LICENSE file in the root directory of this source tree.6 7# Adapted from https://github.com/facebookresearch/segment-anything/blob/main/segment_anything/automatic_mask_generator.py8from typing import Any, Dict, List, Optional, Tuple9 10import numpy as np11import torch12from torchvision.ops.boxes import batched_nms, box_area # type: ignore13 14from sam2.modeling.sam2_base import SAM2Base15from sam2.sam2_image_predictor import SAM2ImagePredictor16from sam2.utils.amg import (17 area_from_rle,18 batch_iterator,19 batched_mask_to_box,20 box_xyxy_to_xywh,21 build_all_layer_point_grids,22 calculate_stability_score,23 coco_encode_rle,24 generate_crop_boxes,25 is_box_near_crop_edge,26 mask_to_rle_pytorch,27 MaskData,28 remove_small_regions,29 rle_to_mask,30 uncrop_boxes_xyxy,31 uncrop_masks,32 uncrop_points,33)34 35 36class SAM2AutomaticMaskGenerator:37 def __init__(38 self,39 model: SAM2Base,40 points_per_side: Optional[int] = 32,41 points_per_batch: int = 64,42 pred_iou_thresh: float = 0.8,43 stability_score_thresh: float = 0.95,44 stability_score_offset: float = 1.0,45 mask_threshold: float = 0.0,46 box_nms_thresh: float = 0.7,47 crop_n_layers: int = 0,48 crop_nms_thresh: float = 0.7,49 crop_overlap_ratio: float = 512 / 1500,50 crop_n_points_downscale_factor: int = 1,51 point_grids: Optional[List[np.ndarray]] = None,52 min_mask_region_area: int = 0,53 output_mode: str = "binary_mask",54 use_m2m: bool = False,55 multimask_output: bool = True,56 **kwargs,57 ) -> None:58 """59 Using a SAM 2 model, generates masks for the entire image.60 Generates a grid of point prompts over the image, then filters61 low quality and duplicate masks. The default settings are chosen62 for SAM 2 with a HieraL backbone.63 64 Arguments:65 model (Sam): The SAM 2 model to use for mask prediction.66 points_per_side (int or None): The number of points to be sampled67 along one side of the image. The total number of points is68 points_per_side**2. If None, 'point_grids' must provide explicit69 point sampling.70 points_per_batch (int): Sets the number of points run simultaneously71 by the model. Higher numbers may be faster but use more GPU memory.72 pred_iou_thresh (float): A filtering threshold in [0,1], using the73 model's predicted mask quality.74 stability_score_thresh (float): A filtering threshold in [0,1], using75 the stability of the mask under changes to the cutoff used to binarize76 the model's mask predictions.77 stability_score_offset (float): The amount to shift the cutoff when78 calculated the stability score.79 mask_threshold (float): Threshold for binarizing the mask logits80 box_nms_thresh (float): The box IoU cutoff used by non-maximal81 suppression to filter duplicate masks.82 crop_n_layers (int): If >0, mask prediction will be run again on83 crops of the image. Sets the number of layers to run, where each84 layer has 2**i_layer number of image crops.85 crop_nms_thresh (float): The box IoU cutoff used by non-maximal86 suppression to filter duplicate masks between different crops.87 crop_overlap_ratio (float): Sets the degree to which crops overlap.88 In the first crop layer, crops will overlap by this fraction of89 the image length. Later layers with more crops scale down this overlap.90 crop_n_points_downscale_factor (int): The number of points-per-side91 sampled in layer n is scaled down by crop_n_points_downscale_factor**n.92 point_grids (list(np.ndarray) or None): A list over explicit grids93 of points used for sampling, normalized to [0,1]. The nth grid in the94 list is used in the nth crop layer. Exclusive with points_per_side.95 min_mask_region_area (int): If >0, postprocessing will be applied96 to remove disconnected regions and holes in masks with area smaller97 than min_mask_region_area. Requires opencv.98 output_mode (str): The form masks are returned in. Can be 'binary_mask',99 'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools.100 For large resolutions, 'binary_mask' may consume large amounts of101 memory.102 use_m2m (bool): Whether to add a one step refinement using previous mask predictions.103 multimask_output (bool): Whether to output multimask at each point of the grid.104 """105 106 assert (points_per_side is None) != (107 point_grids is None108 ), "Exactly one of points_per_side or point_grid must be provided."109 if points_per_side is not None:110 self.point_grids = build_all_layer_point_grids(111 points_per_side,112 crop_n_layers,113 crop_n_points_downscale_factor,114 )115 elif point_grids is not None:116 self.point_grids = point_grids117 else:118 raise ValueError("Can't have both points_per_side and point_grid be None.")119 120 assert output_mode in [121 "binary_mask",122 "uncompressed_rle",123 "coco_rle",124 ], f"Unknown output_mode {output_mode}."125 if output_mode == "coco_rle":126 try:127 from pycocotools import mask as mask_utils # type: ignore # noqa: F401128 except ImportError as e:129 print("Please install pycocotools")130 raise e131 132 self.predictor = SAM2ImagePredictor(133 model,134 max_hole_area=min_mask_region_area,135 max_sprinkle_area=min_mask_region_area,136 )137 self.points_per_batch = points_per_batch138 self.pred_iou_thresh = pred_iou_thresh139 self.stability_score_thresh = stability_score_thresh140 self.stability_score_offset = stability_score_offset141 self.mask_threshold = mask_threshold142 self.box_nms_thresh = box_nms_thresh143 self.crop_n_layers = crop_n_layers144 self.crop_nms_thresh = crop_nms_thresh145 self.crop_overlap_ratio = crop_overlap_ratio146 self.crop_n_points_downscale_factor = crop_n_points_downscale_factor147 self.min_mask_region_area = min_mask_region_area148 self.output_mode = output_mode149 self.use_m2m = use_m2m150 self.multimask_output = multimask_output151 152 @classmethod153 def from_pretrained(cls, model_id: str, **kwargs) -> "SAM2AutomaticMaskGenerator":154 """155 Load a pretrained model from the Hugging Face hub.156 157 Arguments:158 model_id (str): The Hugging Face repository ID.159 **kwargs: Additional arguments to pass to the model constructor.160 161 Returns:162 (SAM2AutomaticMaskGenerator): The loaded model.163 """164 from sam2.build_sam import build_sam2_hf165 166 sam_model = build_sam2_hf(model_id, **kwargs)167 return cls(sam_model, **kwargs)168 169 @torch.no_grad()170 def generate(self, image: np.ndarray) -> List[Dict[str, Any]]:171 """172 Generates masks for the given image.173 174 Arguments:175 image (np.ndarray): The image to generate masks for, in HWC uint8 format.176 177 Returns:178 list(dict(str, any)): A list over records for masks. Each record is179 a dict containing the following keys:180 segmentation (dict(str, any) or np.ndarray): The mask. If181 output_mode='binary_mask', is an array of shape HW. Otherwise,182 is a dictionary containing the RLE.183 bbox (list(float)): The box around the mask, in XYWH format.184 area (int): The area in pixels of the mask.185 predicted_iou (float): The model's own prediction of the mask's186 quality. This is filtered by the pred_iou_thresh parameter.187 point_coords (list(list(float))): The point coordinates input188 to the model to generate this mask.189 stability_score (float): A measure of the mask's quality. This190 is filtered on using the stability_score_thresh parameter.191 crop_box (list(float)): The crop of the image used to generate192 the mask, given in XYWH format.193 """194 195 # Generate masks196 mask_data = self._generate_masks(image)197 198 # Encode masks199 if self.output_mode == "coco_rle":200 mask_data["segmentations"] = [201 coco_encode_rle(rle) for rle in mask_data["rles"]202 ]203 elif self.output_mode == "binary_mask":204 mask_data["segmentations"] = [rle_to_mask(rle) for rle in mask_data["rles"]]205 else:206 mask_data["segmentations"] = mask_data["rles"]207 208 # Write mask records209 curr_anns = []210 for idx in range(len(mask_data["segmentations"])):211 ann = {212 "segmentation": mask_data["segmentations"][idx],213 "area": area_from_rle(mask_data["rles"][idx]),214 "bbox": box_xyxy_to_xywh(mask_data["boxes"][idx]).tolist(),215 "predicted_iou": mask_data["iou_preds"][idx].item(),216 "point_coords": [mask_data["points"][idx].tolist()],217 "stability_score": mask_data["stability_score"][idx].item(),218 "crop_box": box_xyxy_to_xywh(mask_data["crop_boxes"][idx]).tolist(),219 }220 curr_anns.append(ann)221 222 return curr_anns223 224 def _generate_masks(self, image: np.ndarray) -> MaskData:225 orig_size = image.shape[:2]226 crop_boxes, layer_idxs = generate_crop_boxes(227 orig_size, self.crop_n_layers, self.crop_overlap_ratio228 )229 230 # Iterate over image crops231 data = MaskData()232 for crop_box, layer_idx in zip(crop_boxes, layer_idxs):233 crop_data = self._process_crop(image, crop_box, layer_idx, orig_size)234 data.cat(crop_data)235 236 # Remove duplicate masks between crops237 if len(crop_boxes) > 1:238 # Prefer masks from smaller crops239 scores = 1 / box_area(data["crop_boxes"])240 scores = scores.to(data["boxes"].device)241 keep_by_nms = batched_nms(242 data["boxes"].float(),243 scores,244 torch.zeros_like(data["boxes"][:, 0]), # categories245 iou_threshold=self.crop_nms_thresh,246 )247 data.filter(keep_by_nms)248 data.to_numpy()249 return data250 251 def _process_crop(252 self,253 image: np.ndarray,254 crop_box: List[int],255 crop_layer_idx: int,256 orig_size: Tuple[int, ...],257 ) -> MaskData:258 # Crop the image and calculate embeddings259 x0, y0, x1, y1 = crop_box260 cropped_im = image[y0:y1, x0:x1, :]261 cropped_im_size = cropped_im.shape[:2]262 self.predictor.set_image(cropped_im)263 264 # Get points for this crop265 points_scale = np.array(cropped_im_size)[None, ::-1]266 points_for_image = self.point_grids[crop_layer_idx] * points_scale267 268 # Generate masks for this crop in batches269 data = MaskData()270 for (points,) in batch_iterator(self.points_per_batch, points_for_image):271 batch_data = self._process_batch(272 points, cropped_im_size, crop_box, orig_size, normalize=True273 )274 data.cat(batch_data)275 del batch_data276 self.predictor.reset_predictor()277 278 # Remove duplicates within this crop.279 keep_by_nms = batched_nms(280 data["boxes"].float(),281 data["iou_preds"],282 torch.zeros_like(data["boxes"][:, 0]), # categories283 iou_threshold=self.box_nms_thresh,284 )285 data.filter(keep_by_nms)286 287 # Return to the original image frame288 data["boxes"] = uncrop_boxes_xyxy(data["boxes"], crop_box)289 data["points"] = uncrop_points(data["points"], crop_box)290 data["crop_boxes"] = torch.tensor([crop_box for _ in range(len(data["rles"]))])291 292 return data293 294 def _process_batch(295 self,296 points: np.ndarray,297 im_size: Tuple[int, ...],298 crop_box: List[int],299 orig_size: Tuple[int, ...],300 normalize=False,301 ) -> MaskData:302 orig_h, orig_w = orig_size303 304 # Run model on this batch305 points = torch.as_tensor(306 points, dtype=torch.float32, device=self.predictor.device307 )308 in_points = self.predictor._transforms.transform_coords(309 points, normalize=normalize, orig_hw=im_size310 )311 in_labels = torch.ones(312 in_points.shape[0], dtype=torch.int, device=in_points.device313 )314 masks, iou_preds, low_res_masks = self.predictor._predict(315 in_points[:, None, :],316 in_labels[:, None],317 multimask_output=self.multimask_output,318 return_logits=True,319 )320 321 # Serialize predictions and store in MaskData322 data = MaskData(323 masks=masks.flatten(0, 1),324 iou_preds=iou_preds.flatten(0, 1),325 points=points.repeat_interleave(masks.shape[1], dim=0),326 low_res_masks=low_res_masks.flatten(0, 1),327 )328 del masks329 330 if not self.use_m2m:331 # Filter by predicted IoU332 if self.pred_iou_thresh > 0.0:333 keep_mask = data["iou_preds"] > self.pred_iou_thresh334 data.filter(keep_mask)335 336 # Calculate and filter by stability score337 data["stability_score"] = calculate_stability_score(338 data["masks"], self.mask_threshold, self.stability_score_offset339 )340 if self.stability_score_thresh > 0.0:341 keep_mask = data["stability_score"] >= self.stability_score_thresh342 data.filter(keep_mask)343 else:344 # One step refinement using previous mask predictions345 in_points = self.predictor._transforms.transform_coords(346 data["points"], normalize=normalize, orig_hw=im_size347 )348 labels = torch.ones(349 in_points.shape[0], dtype=torch.int, device=in_points.device350 )351 masks, ious = self.refine_with_m2m(352 in_points, labels, data["low_res_masks"], self.points_per_batch353 )354 data["masks"] = masks.squeeze(1)355 data["iou_preds"] = ious.squeeze(1)356 357 if self.pred_iou_thresh > 0.0:358 keep_mask = data["iou_preds"] > self.pred_iou_thresh359 data.filter(keep_mask)360 361 data["stability_score"] = calculate_stability_score(362 data["masks"], self.mask_threshold, self.stability_score_offset363 )364 if self.stability_score_thresh > 0.0:365 keep_mask = data["stability_score"] >= self.stability_score_thresh366 data.filter(keep_mask)367 368 # Threshold masks and calculate boxes369 data["masks"] = data["masks"] > self.mask_threshold370 data["boxes"] = batched_mask_to_box(data["masks"])371 372 # Filter boxes that touch crop boundaries373 keep_mask = ~is_box_near_crop_edge(374 data["boxes"], crop_box, [0, 0, orig_w, orig_h]375 )376 if not torch.all(keep_mask):377 data.filter(keep_mask)378 379 # Compress to RLE380 data["masks"] = uncrop_masks(data["masks"], crop_box, orig_h, orig_w)381 data["rles"] = mask_to_rle_pytorch(data["masks"])382 del data["masks"]383 384 return data385 386 @staticmethod387 def postprocess_small_regions(388 mask_data: MaskData, min_area: int, nms_thresh: float389 ) -> MaskData:390 """391 Removes small disconnected regions and holes in masks, then reruns392 box NMS to remove any new duplicates.393 394 Edits mask_data in place.395 396 Requires open-cv as a dependency.397 """398 if len(mask_data["rles"]) == 0:399 return mask_data400 401 # Filter small disconnected regions and holes402 new_masks = []403 scores = []404 for rle in mask_data["rles"]:405 mask = rle_to_mask(rle)406 407 mask, changed = remove_small_regions(mask, min_area, mode="holes")408 unchanged = not changed409 mask, changed = remove_small_regions(mask, min_area, mode="islands")410 unchanged = unchanged and not changed411 412 new_masks.append(torch.as_tensor(mask).unsqueeze(0))413 # Give score=0 to changed masks and score=1 to unchanged masks414 # so NMS will prefer ones that didn't need postprocessing415 scores.append(float(unchanged))416 417 # Recalculate boxes and remove any new duplicates418 masks = torch.cat(new_masks, dim=0)419 boxes = batched_mask_to_box(masks)420 keep_by_nms = batched_nms(421 boxes.float(),422 torch.as_tensor(scores),423 torch.zeros_like(boxes[:, 0]), # categories424 iou_threshold=nms_thresh,425 )426 427 # Only recalculate RLEs for masks that have changed428 for i_mask in keep_by_nms:429 if scores[i_mask] == 0.0:430 mask_torch = masks[i_mask].unsqueeze(0)431 mask_data["rles"][i_mask] = mask_to_rle_pytorch(mask_torch)[0]432 mask_data["boxes"][i_mask] = boxes[i_mask] # update res directly433 mask_data.filter(keep_by_nms)434 435 return mask_data436 437 def refine_with_m2m(self, points, point_labels, low_res_masks, points_per_batch):438 new_masks = []439 new_iou_preds = []440 441 for cur_points, cur_point_labels, low_res_mask in batch_iterator(442 points_per_batch, points, point_labels, low_res_masks443 ):444 best_masks, best_iou_preds, _ = self.predictor._predict(445 cur_points[:, None, :],446 cur_point_labels[:, None],447 mask_input=low_res_mask[:, None, :],448 multimask_output=False,449 return_logits=True,450 )451 new_masks.append(best_masks)452 new_iou_preds.append(best_iou_preds)453 masks = torch.cat(new_masks, dim=0)454 return masks, torch.cat(new_iou_preds, dim=0)455 