CoolFace
Apppublic

ZiyuG/SAM2Point

sourceHugging Faceapache-2.0updated 2y agoView on Hugging Face
16likes
automatic_mask_generator.py435 linesDownload Raw Back to sam2
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    ) -> None:57        """58        Using a SAM 2 model, generates masks for the entire image.59        Generates a grid of point prompts over the image, then filters60        low quality and duplicate masks. The default settings are chosen61        for SAM 2 with a HieraL backbone.62 63        Arguments:64          model (Sam): The SAM 2 model to use for mask prediction.65          points_per_side (int or None): The number of points to be sampled66            along one side of the image. The total number of points is67            points_per_side**2. If None, 'point_grids' must provide explicit68            point sampling.69          points_per_batch (int): Sets the number of points run simultaneously70            by the model. Higher numbers may be faster but use more GPU memory.71          pred_iou_thresh (float): A filtering threshold in [0,1], using the72            model's predicted mask quality.73          stability_score_thresh (float): A filtering threshold in [0,1], using74            the stability of the mask under changes to the cutoff used to binarize75            the model's mask predictions.76          stability_score_offset (float): The amount to shift the cutoff when77            calculated the stability score.78          mask_threshold (float): Threshold for binarizing the mask logits79          box_nms_thresh (float): The box IoU cutoff used by non-maximal80            suppression to filter duplicate masks.81          crop_n_layers (int): If >0, mask prediction will be run again on82            crops of the image. Sets the number of layers to run, where each83            layer has 2**i_layer number of image crops.84          crop_nms_thresh (float): The box IoU cutoff used by non-maximal85            suppression to filter duplicate masks between different crops.86          crop_overlap_ratio (float): Sets the degree to which crops overlap.87            In the first crop layer, crops will overlap by this fraction of88            the image length. Later layers with more crops scale down this overlap.89          crop_n_points_downscale_factor (int): The number of points-per-side90            sampled in layer n is scaled down by crop_n_points_downscale_factor**n.91          point_grids (list(np.ndarray) or None): A list over explicit grids92            of points used for sampling, normalized to [0,1]. The nth grid in the93            list is used in the nth crop layer. Exclusive with points_per_side.94          min_mask_region_area (int): If >0, postprocessing will be applied95            to remove disconnected regions and holes in masks with area smaller96            than min_mask_region_area. Requires opencv.97          output_mode (str): The form masks are returned in. Can be 'binary_mask',98            'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools.99            For large resolutions, 'binary_mask' may consume large amounts of100            memory.101          use_m2m (bool): Whether to add a one step refinement using previous mask predictions.102          multimask_output (bool): Whether to output multimask at each point of the grid.103        """104 105        assert (points_per_side is None) != (106            point_grids is None107        ), "Exactly one of points_per_side or point_grid must be provided."108        if points_per_side is not None:109            self.point_grids = build_all_layer_point_grids(110                points_per_side,111                crop_n_layers,112                crop_n_points_downscale_factor,113            )114        elif point_grids is not None:115            self.point_grids = point_grids116        else:117            raise ValueError("Can't have both points_per_side and point_grid be None.")118 119        assert output_mode in [120            "binary_mask",121            "uncompressed_rle",122            "coco_rle",123        ], f"Unknown output_mode {output_mode}."124        if output_mode == "coco_rle":125            try:126                from pycocotools import mask as mask_utils  # type: ignore  # noqa: F401127            except ImportError as e:128                print("Please install pycocotools")129                raise e130 131        self.predictor = SAM2ImagePredictor(132            model,133            max_hole_area=min_mask_region_area,134            max_sprinkle_area=min_mask_region_area,135        )136        self.points_per_batch = points_per_batch137        self.pred_iou_thresh = pred_iou_thresh138        self.stability_score_thresh = stability_score_thresh139        self.stability_score_offset = stability_score_offset140        self.mask_threshold = mask_threshold141        self.box_nms_thresh = box_nms_thresh142        self.crop_n_layers = crop_n_layers143        self.crop_nms_thresh = crop_nms_thresh144        self.crop_overlap_ratio = crop_overlap_ratio145        self.crop_n_points_downscale_factor = crop_n_points_downscale_factor146        self.min_mask_region_area = min_mask_region_area147        self.output_mode = output_mode148        self.use_m2m = use_m2m149        self.multimask_output = multimask_output150 151    @torch.no_grad()152    def generate(self, image: np.ndarray) -> List[Dict[str, Any]]:153        """154        Generates masks for the given image.155 156        Arguments:157          image (np.ndarray): The image to generate masks for, in HWC uint8 format.158 159        Returns:160           list(dict(str, any)): A list over records for masks. Each record is161             a dict containing the following keys:162               segmentation (dict(str, any) or np.ndarray): The mask. If163                 output_mode='binary_mask', is an array of shape HW. Otherwise,164                 is a dictionary containing the RLE.165               bbox (list(float)): The box around the mask, in XYWH format.166               area (int): The area in pixels of the mask.167               predicted_iou (float): The model's own prediction of the mask's168                 quality. This is filtered by the pred_iou_thresh parameter.169               point_coords (list(list(float))): The point coordinates input170                 to the model to generate this mask.171               stability_score (float): A measure of the mask's quality. This172                 is filtered on using the stability_score_thresh parameter.173               crop_box (list(float)): The crop of the image used to generate174                 the mask, given in XYWH format.175        """176 177        # Generate masks178        mask_data = self._generate_masks(image)179 180        # Encode masks181        if self.output_mode == "coco_rle":182            mask_data["segmentations"] = [183                coco_encode_rle(rle) for rle in mask_data["rles"]184            ]185        elif self.output_mode == "binary_mask":186            mask_data["segmentations"] = [rle_to_mask(rle) for rle in mask_data["rles"]]187        else:188            mask_data["segmentations"] = mask_data["rles"]189 190        # Write mask records191        curr_anns = []192        for idx in range(len(mask_data["segmentations"])):193            ann = {194                "segmentation": mask_data["segmentations"][idx],195                "area": area_from_rle(mask_data["rles"][idx]),196                "bbox": box_xyxy_to_xywh(mask_data["boxes"][idx]).tolist(),197                "predicted_iou": mask_data["iou_preds"][idx].item(),198                "point_coords": [mask_data["points"][idx].tolist()],199                "stability_score": mask_data["stability_score"][idx].item(),200                "crop_box": box_xyxy_to_xywh(mask_data["crop_boxes"][idx]).tolist(),201            }202            curr_anns.append(ann)203 204        return curr_anns205 206    def _generate_masks(self, image: np.ndarray) -> MaskData:207        orig_size = image.shape[:2]208        crop_boxes, layer_idxs = generate_crop_boxes(209            orig_size, self.crop_n_layers, self.crop_overlap_ratio210        )211 212        # Iterate over image crops213        data = MaskData()214        for crop_box, layer_idx in zip(crop_boxes, layer_idxs):215            crop_data = self._process_crop(image, crop_box, layer_idx, orig_size)216            data.cat(crop_data)217 218        # Remove duplicate masks between crops219        if len(crop_boxes) > 1:220            # Prefer masks from smaller crops221            scores = 1 / box_area(data["crop_boxes"])222            scores = scores.to(data["boxes"].device)223            keep_by_nms = batched_nms(224                data["boxes"].float(),225                scores,226                torch.zeros_like(data["boxes"][:, 0]),  # categories227                iou_threshold=self.crop_nms_thresh,228            )229            data.filter(keep_by_nms)230        data.to_numpy()231        return data232 233    def _process_crop(234        self,235        image: np.ndarray,236        crop_box: List[int],237        crop_layer_idx: int,238        orig_size: Tuple[int, ...],239    ) -> MaskData:240        # Crop the image and calculate embeddings241        x0, y0, x1, y1 = crop_box242        cropped_im = image[y0:y1, x0:x1, :]243        cropped_im_size = cropped_im.shape[:2]244        self.predictor.set_image(cropped_im)245 246        # Get points for this crop247        points_scale = np.array(cropped_im_size)[None, ::-1]248        points_for_image = self.point_grids[crop_layer_idx] * points_scale249 250        # Generate masks for this crop in batches251        data = MaskData()252        for (points,) in batch_iterator(self.points_per_batch, points_for_image):253            batch_data = self._process_batch(254                points, cropped_im_size, crop_box, orig_size, normalize=True255            )256            data.cat(batch_data)257            del batch_data258        self.predictor.reset_predictor()259 260        # Remove duplicates within this crop.261        keep_by_nms = batched_nms(262            data["boxes"].float(),263            data["iou_preds"],264            torch.zeros_like(data["boxes"][:, 0]),  # categories265            iou_threshold=self.box_nms_thresh,266        )267        data.filter(keep_by_nms)268 269        # Return to the original image frame270        data["boxes"] = uncrop_boxes_xyxy(data["boxes"], crop_box)271        data["points"] = uncrop_points(data["points"], crop_box)272        data["crop_boxes"] = torch.tensor([crop_box for _ in range(len(data["rles"]))])273 274        return data275 276    def _process_batch(277        self,278        points: np.ndarray,279        im_size: Tuple[int, ...],280        crop_box: List[int],281        orig_size: Tuple[int, ...],282        normalize=False,283    ) -> MaskData:284        orig_h, orig_w = orig_size285 286        # Run model on this batch287        points = torch.as_tensor(points, device=self.predictor.device)288        in_points = self.predictor._transforms.transform_coords(289            points, normalize=normalize, orig_hw=im_size290        )291        in_labels = torch.ones(292            in_points.shape[0], dtype=torch.int, device=in_points.device293        )294        masks, iou_preds, low_res_masks = self.predictor._predict(295            in_points[:, None, :],296            in_labels[:, None],297            multimask_output=self.multimask_output,298            return_logits=True,299        )300 301        # Serialize predictions and store in MaskData302        data = MaskData(303            masks=masks.flatten(0, 1),304            iou_preds=iou_preds.flatten(0, 1),305            points=points.repeat_interleave(masks.shape[1], dim=0),306            low_res_masks=low_res_masks.flatten(0, 1),307        )308        del masks309 310        if not self.use_m2m:311            # Filter by predicted IoU312            if self.pred_iou_thresh > 0.0:313                keep_mask = data["iou_preds"] > self.pred_iou_thresh314                data.filter(keep_mask)315 316            # Calculate and filter by stability score317            data["stability_score"] = calculate_stability_score(318                data["masks"], self.mask_threshold, self.stability_score_offset319            )320            if self.stability_score_thresh > 0.0:321                keep_mask = data["stability_score"] >= self.stability_score_thresh322                data.filter(keep_mask)323        else:324            # One step refinement using previous mask predictions325            in_points = self.predictor._transforms.transform_coords(326                data["points"], normalize=normalize, orig_hw=im_size327            )328            labels = torch.ones(329                in_points.shape[0], dtype=torch.int, device=in_points.device330            )331            masks, ious = self.refine_with_m2m(332                in_points, labels, data["low_res_masks"], self.points_per_batch333            )334            data["masks"] = masks.squeeze(1)335            data["iou_preds"] = ious.squeeze(1)336 337            if self.pred_iou_thresh > 0.0:338                keep_mask = data["iou_preds"] > self.pred_iou_thresh339                data.filter(keep_mask)340 341            data["stability_score"] = calculate_stability_score(342                data["masks"], self.mask_threshold, self.stability_score_offset343            )344            if self.stability_score_thresh > 0.0:345                keep_mask = data["stability_score"] >= self.stability_score_thresh346                data.filter(keep_mask)347 348        # Threshold masks and calculate boxes349        data["masks"] = data["masks"] > self.mask_threshold350        data["boxes"] = batched_mask_to_box(data["masks"])351 352        # Filter boxes that touch crop boundaries353        keep_mask = ~is_box_near_crop_edge(354            data["boxes"], crop_box, [0, 0, orig_w, orig_h]355        )356        if not torch.all(keep_mask):357            data.filter(keep_mask)358 359        # Compress to RLE360        data["masks"] = uncrop_masks(data["masks"], crop_box, orig_h, orig_w)361        data["rles"] = mask_to_rle_pytorch(data["masks"])362        del data["masks"]363 364        return data365 366    @staticmethod367    def postprocess_small_regions(368        mask_data: MaskData, min_area: int, nms_thresh: float369    ) -> MaskData:370        """371        Removes small disconnected regions and holes in masks, then reruns372        box NMS to remove any new duplicates.373 374        Edits mask_data in place.375 376        Requires open-cv as a dependency.377        """378        if len(mask_data["rles"]) == 0:379            return mask_data380 381        # Filter small disconnected regions and holes382        new_masks = []383        scores = []384        for rle in mask_data["rles"]:385            mask = rle_to_mask(rle)386 387            mask, changed = remove_small_regions(mask, min_area, mode="holes")388            unchanged = not changed389            mask, changed = remove_small_regions(mask, min_area, mode="islands")390            unchanged = unchanged and not changed391 392            new_masks.append(torch.as_tensor(mask).unsqueeze(0))393            # Give score=0 to changed masks and score=1 to unchanged masks394            # so NMS will prefer ones that didn't need postprocessing395            scores.append(float(unchanged))396 397        # Recalculate boxes and remove any new duplicates398        masks = torch.cat(new_masks, dim=0)399        boxes = batched_mask_to_box(masks)400        keep_by_nms = batched_nms(401            boxes.float(),402            torch.as_tensor(scores),403            torch.zeros_like(boxes[:, 0]),  # categories404            iou_threshold=nms_thresh,405        )406 407        # Only recalculate RLEs for masks that have changed408        for i_mask in keep_by_nms:409            if scores[i_mask] == 0.0:410                mask_torch = masks[i_mask].unsqueeze(0)411                mask_data["rles"][i_mask] = mask_to_rle_pytorch(mask_torch)[0]412                mask_data["boxes"][i_mask] = boxes[i_mask]  # update res directly413        mask_data.filter(keep_by_nms)414 415        return mask_data416 417    def refine_with_m2m(self, points, point_labels, low_res_masks, points_per_batch):418        new_masks = []419        new_iou_preds = []420 421        for cur_points, cur_point_labels, low_res_mask in batch_iterator(422            points_per_batch, points, point_labels, low_res_masks423        ):424            best_masks, best_iou_preds, _ = self.predictor._predict(425                cur_points[:, None, :],426                cur_point_labels[:, None],427                mask_input=low_res_mask[:, None, :],428                multimask_output=False,429                return_logits=True,430            )431            new_masks.append(best_masks)432            new_iou_preds.append(best_iou_preds)433        masks = torch.cat(new_masks, dim=0)434        return masks, torch.cat(new_iou_preds, dim=0)435