|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import numpy as np
|
|
import torch
|
|
from torchvision.ops.boxes import batched_nms, box_area
|
|
|
|
from sam2.modeling.sam2_base import SAM2Base
|
|
from sam2.sam2_image_predictor import SAM2ImagePredictor
|
|
from sam2.utils.amg import (
|
|
area_from_rle,
|
|
batch_iterator,
|
|
batched_mask_to_box,
|
|
box_xyxy_to_xywh,
|
|
build_all_layer_point_grids,
|
|
calculate_stability_score,
|
|
coco_encode_rle,
|
|
generate_crop_boxes,
|
|
is_box_near_crop_edge,
|
|
mask_to_rle_pytorch,
|
|
MaskData,
|
|
remove_small_regions,
|
|
rle_to_mask,
|
|
uncrop_boxes_xyxy,
|
|
uncrop_masks,
|
|
uncrop_points,
|
|
)
|
|
|
|
|
|
class SAM2AutomaticMaskGenerator:
|
|
def __init__(
|
|
self,
|
|
model: SAM2Base,
|
|
points_per_side: Optional[int] = 32,
|
|
points_per_batch: int = 64,
|
|
pred_iou_thresh: float = 0.8,
|
|
stability_score_thresh: float = 0.95,
|
|
stability_score_offset: float = 1.0,
|
|
mask_threshold: float = 0.0,
|
|
box_nms_thresh: float = 0.7,
|
|
crop_n_layers: int = 0,
|
|
crop_nms_thresh: float = 0.7,
|
|
crop_overlap_ratio: float = 512 / 1500,
|
|
crop_n_points_downscale_factor: int = 1,
|
|
point_grids: Optional[List[np.ndarray]] = None,
|
|
min_mask_region_area: int = 0,
|
|
output_mode: str = "binary_mask",
|
|
use_m2m: bool = False,
|
|
multimask_output: bool = True,
|
|
**kwargs,
|
|
) -> None:
|
|
"""
|
|
Using a SAM 2 model, generates masks for the entire image.
|
|
Generates a grid of point prompts over the image, then filters
|
|
low quality and duplicate masks. The default settings are chosen
|
|
for SAM 2 with a HieraL backbone.
|
|
|
|
Arguments:
|
|
model (Sam): The SAM 2 model to use for mask prediction.
|
|
points_per_side (int or None): The number of points to be sampled
|
|
along one side of the image. The total number of points is
|
|
points_per_side**2. If None, 'point_grids' must provide explicit
|
|
point sampling.
|
|
points_per_batch (int): Sets the number of points run simultaneously
|
|
by the model. Higher numbers may be faster but use more GPU memory.
|
|
pred_iou_thresh (float): A filtering threshold in [0,1], using the
|
|
model's predicted mask quality.
|
|
stability_score_thresh (float): A filtering threshold in [0,1], using
|
|
the stability of the mask under changes to the cutoff used to binarize
|
|
the model's mask predictions.
|
|
stability_score_offset (float): The amount to shift the cutoff when
|
|
calculated the stability score.
|
|
mask_threshold (float): Threshold for binarizing the mask logits
|
|
box_nms_thresh (float): The box IoU cutoff used by non-maximal
|
|
suppression to filter duplicate masks.
|
|
crop_n_layers (int): If >0, mask prediction will be run again on
|
|
crops of the image. Sets the number of layers to run, where each
|
|
layer has 2**i_layer number of image crops.
|
|
crop_nms_thresh (float): The box IoU cutoff used by non-maximal
|
|
suppression to filter duplicate masks between different crops.
|
|
crop_overlap_ratio (float): Sets the degree to which crops overlap.
|
|
In the first crop layer, crops will overlap by this fraction of
|
|
the image length. Later layers with more crops scale down this overlap.
|
|
crop_n_points_downscale_factor (int): The number of points-per-side
|
|
sampled in layer n is scaled down by crop_n_points_downscale_factor**n.
|
|
point_grids (list(np.ndarray) or None): A list over explicit grids
|
|
of points used for sampling, normalized to [0,1]. The nth grid in the
|
|
list is used in the nth crop layer. Exclusive with points_per_side.
|
|
min_mask_region_area (int): If >0, postprocessing will be applied
|
|
to remove disconnected regions and holes in masks with area smaller
|
|
than min_mask_region_area. Requires opencv.
|
|
output_mode (str): The form masks are returned in. Can be 'binary_mask',
|
|
'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools.
|
|
For large resolutions, 'binary_mask' may consume large amounts of
|
|
memory.
|
|
use_m2m (bool): Whether to add a one step refinement using previous mask predictions.
|
|
multimask_output (bool): Whether to output multimask at each point of the grid.
|
|
"""
|
|
|
|
assert (points_per_side is None) != (
|
|
point_grids is None
|
|
), "Exactly one of points_per_side or point_grid must be provided."
|
|
if points_per_side is not None:
|
|
self.point_grids = build_all_layer_point_grids(
|
|
points_per_side,
|
|
crop_n_layers,
|
|
crop_n_points_downscale_factor,
|
|
)
|
|
elif point_grids is not None:
|
|
self.point_grids = point_grids
|
|
else:
|
|
raise ValueError("Can't have both points_per_side and point_grid be None.")
|
|
|
|
assert output_mode in [
|
|
"binary_mask",
|
|
"uncompressed_rle",
|
|
"coco_rle",
|
|
], f"Unknown output_mode {output_mode}."
|
|
if output_mode == "coco_rle":
|
|
try:
|
|
from pycocotools import mask as mask_utils
|
|
except ImportError as e:
|
|
print("Please install pycocotools")
|
|
raise e
|
|
|
|
self.predictor = SAM2ImagePredictor(
|
|
model,
|
|
max_hole_area=min_mask_region_area,
|
|
max_sprinkle_area=min_mask_region_area,
|
|
)
|
|
self.points_per_batch = points_per_batch
|
|
self.pred_iou_thresh = pred_iou_thresh
|
|
self.stability_score_thresh = stability_score_thresh
|
|
self.stability_score_offset = stability_score_offset
|
|
self.mask_threshold = mask_threshold
|
|
self.box_nms_thresh = box_nms_thresh
|
|
self.crop_n_layers = crop_n_layers
|
|
self.crop_nms_thresh = crop_nms_thresh
|
|
self.crop_overlap_ratio = crop_overlap_ratio
|
|
self.crop_n_points_downscale_factor = crop_n_points_downscale_factor
|
|
self.min_mask_region_area = min_mask_region_area
|
|
self.output_mode = output_mode
|
|
self.use_m2m = use_m2m
|
|
self.multimask_output = multimask_output
|
|
|
|
@classmethod
|
|
def from_pretrained(cls, model_id: str, **kwargs) -> "SAM2AutomaticMaskGenerator":
|
|
"""
|
|
Load a pretrained model from the Hugging Face hub.
|
|
|
|
Arguments:
|
|
model_id (str): The Hugging Face repository ID.
|
|
**kwargs: Additional arguments to pass to the model constructor.
|
|
|
|
Returns:
|
|
(SAM2AutomaticMaskGenerator): The loaded model.
|
|
"""
|
|
from sam2.build_sam import build_sam2_hf
|
|
|
|
sam_model = build_sam2_hf(model_id, **kwargs)
|
|
return cls(sam_model, **kwargs)
|
|
|
|
@torch.no_grad()
|
|
def generate(self, image: np.ndarray) -> List[Dict[str, Any]]:
|
|
"""
|
|
Generates masks for the given image.
|
|
|
|
Arguments:
|
|
image (np.ndarray): The image to generate masks for, in HWC uint8 format.
|
|
|
|
Returns:
|
|
list(dict(str, any)): A list over records for masks. Each record is
|
|
a dict containing the following keys:
|
|
segmentation (dict(str, any) or np.ndarray): The mask. If
|
|
output_mode='binary_mask', is an array of shape HW. Otherwise,
|
|
is a dictionary containing the RLE.
|
|
bbox (list(float)): The box around the mask, in XYWH format.
|
|
area (int): The area in pixels of the mask.
|
|
predicted_iou (float): The model's own prediction of the mask's
|
|
quality. This is filtered by the pred_iou_thresh parameter.
|
|
point_coords (list(list(float))): The point coordinates input
|
|
to the model to generate this mask.
|
|
stability_score (float): A measure of the mask's quality. This
|
|
is filtered on using the stability_score_thresh parameter.
|
|
crop_box (list(float)): The crop of the image used to generate
|
|
the mask, given in XYWH format.
|
|
"""
|
|
|
|
|
|
mask_data = self._generate_masks(image)
|
|
|
|
|
|
if self.output_mode == "coco_rle":
|
|
mask_data["segmentations"] = [
|
|
coco_encode_rle(rle) for rle in mask_data["rles"]
|
|
]
|
|
elif self.output_mode == "binary_mask":
|
|
mask_data["segmentations"] = [rle_to_mask(rle) for rle in mask_data["rles"]]
|
|
else:
|
|
mask_data["segmentations"] = mask_data["rles"]
|
|
|
|
|
|
curr_anns = []
|
|
for idx in range(len(mask_data["segmentations"])):
|
|
ann = {
|
|
"segmentation": mask_data["segmentations"][idx],
|
|
"area": area_from_rle(mask_data["rles"][idx]),
|
|
"bbox": box_xyxy_to_xywh(mask_data["boxes"][idx]).tolist(),
|
|
"predicted_iou": mask_data["iou_preds"][idx].item(),
|
|
"point_coords": [mask_data["points"][idx].tolist()],
|
|
"stability_score": mask_data["stability_score"][idx].item(),
|
|
"crop_box": box_xyxy_to_xywh(mask_data["crop_boxes"][idx]).tolist(),
|
|
}
|
|
curr_anns.append(ann)
|
|
|
|
return curr_anns
|
|
|
|
def _generate_masks(self, image: np.ndarray) -> MaskData:
|
|
orig_size = image.shape[:2]
|
|
crop_boxes, layer_idxs = generate_crop_boxes(
|
|
orig_size, self.crop_n_layers, self.crop_overlap_ratio
|
|
)
|
|
|
|
|
|
data = MaskData()
|
|
for crop_box, layer_idx in zip(crop_boxes, layer_idxs):
|
|
crop_data = self._process_crop(image, crop_box, layer_idx, orig_size)
|
|
data.cat(crop_data)
|
|
|
|
|
|
if len(crop_boxes) > 1:
|
|
|
|
scores = 1 / box_area(data["crop_boxes"])
|
|
scores = scores.to(data["boxes"].device)
|
|
keep_by_nms = batched_nms(
|
|
data["boxes"].float(),
|
|
scores,
|
|
torch.zeros_like(data["boxes"][:, 0]),
|
|
iou_threshold=self.crop_nms_thresh,
|
|
)
|
|
data.filter(keep_by_nms)
|
|
data.to_numpy()
|
|
return data
|
|
|
|
def _process_crop(
|
|
self,
|
|
image: np.ndarray,
|
|
crop_box: List[int],
|
|
crop_layer_idx: int,
|
|
orig_size: Tuple[int, ...],
|
|
) -> MaskData:
|
|
|
|
x0, y0, x1, y1 = crop_box
|
|
cropped_im = image[y0:y1, x0:x1, :]
|
|
cropped_im_size = cropped_im.shape[:2]
|
|
self.predictor.set_image(cropped_im)
|
|
|
|
|
|
points_scale = np.array(cropped_im_size)[None, ::-1]
|
|
points_for_image = self.point_grids[crop_layer_idx] * points_scale
|
|
|
|
|
|
data = MaskData()
|
|
for (points,) in batch_iterator(self.points_per_batch, points_for_image):
|
|
batch_data = self._process_batch(
|
|
points, cropped_im_size, crop_box, orig_size, normalize=True
|
|
)
|
|
data.cat(batch_data)
|
|
del batch_data
|
|
self.predictor.reset_predictor()
|
|
|
|
|
|
keep_by_nms = batched_nms(
|
|
data["boxes"].float(),
|
|
data["iou_preds"],
|
|
torch.zeros_like(data["boxes"][:, 0]),
|
|
iou_threshold=self.box_nms_thresh,
|
|
)
|
|
data.filter(keep_by_nms)
|
|
|
|
|
|
data["boxes"] = uncrop_boxes_xyxy(data["boxes"], crop_box)
|
|
data["points"] = uncrop_points(data["points"], crop_box)
|
|
data["crop_boxes"] = torch.tensor([crop_box for _ in range(len(data["rles"]))])
|
|
|
|
return data
|
|
|
|
def _process_batch(
|
|
self,
|
|
points: np.ndarray,
|
|
im_size: Tuple[int, ...],
|
|
crop_box: List[int],
|
|
orig_size: Tuple[int, ...],
|
|
normalize=False,
|
|
) -> MaskData:
|
|
orig_h, orig_w = orig_size
|
|
|
|
|
|
points = torch.as_tensor(
|
|
points, dtype=torch.float32, device=self.predictor.device
|
|
)
|
|
in_points = self.predictor._transforms.transform_coords(
|
|
points, normalize=normalize, orig_hw=im_size
|
|
)
|
|
in_labels = torch.ones(
|
|
in_points.shape[0], dtype=torch.int, device=in_points.device
|
|
)
|
|
masks, iou_preds, low_res_masks = self.predictor._predict(
|
|
in_points[:, None, :],
|
|
in_labels[:, None],
|
|
multimask_output=self.multimask_output,
|
|
return_logits=True,
|
|
)
|
|
|
|
|
|
data = MaskData(
|
|
masks=masks.flatten(0, 1),
|
|
iou_preds=iou_preds.flatten(0, 1),
|
|
points=points.repeat_interleave(masks.shape[1], dim=0),
|
|
low_res_masks=low_res_masks.flatten(0, 1),
|
|
)
|
|
del masks
|
|
|
|
if not self.use_m2m:
|
|
|
|
if self.pred_iou_thresh > 0.0:
|
|
keep_mask = data["iou_preds"] > self.pred_iou_thresh
|
|
data.filter(keep_mask)
|
|
|
|
|
|
data["stability_score"] = calculate_stability_score(
|
|
data["masks"], self.mask_threshold, self.stability_score_offset
|
|
)
|
|
if self.stability_score_thresh > 0.0:
|
|
keep_mask = data["stability_score"] >= self.stability_score_thresh
|
|
data.filter(keep_mask)
|
|
else:
|
|
|
|
in_points = self.predictor._transforms.transform_coords(
|
|
data["points"], normalize=normalize, orig_hw=im_size
|
|
)
|
|
labels = torch.ones(
|
|
in_points.shape[0], dtype=torch.int, device=in_points.device
|
|
)
|
|
masks, ious = self.refine_with_m2m(
|
|
in_points, labels, data["low_res_masks"], self.points_per_batch
|
|
)
|
|
data["masks"] = masks.squeeze(1)
|
|
data["iou_preds"] = ious.squeeze(1)
|
|
|
|
if self.pred_iou_thresh > 0.0:
|
|
keep_mask = data["iou_preds"] > self.pred_iou_thresh
|
|
data.filter(keep_mask)
|
|
|
|
data["stability_score"] = calculate_stability_score(
|
|
data["masks"], self.mask_threshold, self.stability_score_offset
|
|
)
|
|
if self.stability_score_thresh > 0.0:
|
|
keep_mask = data["stability_score"] >= self.stability_score_thresh
|
|
data.filter(keep_mask)
|
|
|
|
|
|
data["masks"] = data["masks"] > self.mask_threshold
|
|
data["boxes"] = batched_mask_to_box(data["masks"])
|
|
|
|
|
|
keep_mask = ~is_box_near_crop_edge(
|
|
data["boxes"], crop_box, [0, 0, orig_w, orig_h]
|
|
)
|
|
if not torch.all(keep_mask):
|
|
data.filter(keep_mask)
|
|
|
|
|
|
data["masks"] = uncrop_masks(data["masks"], crop_box, orig_h, orig_w)
|
|
data["rles"] = mask_to_rle_pytorch(data["masks"])
|
|
del data["masks"]
|
|
|
|
return data
|
|
|
|
@staticmethod
|
|
def postprocess_small_regions(
|
|
mask_data: MaskData, min_area: int, nms_thresh: float
|
|
) -> MaskData:
|
|
"""
|
|
Removes small disconnected regions and holes in masks, then reruns
|
|
box NMS to remove any new duplicates.
|
|
|
|
Edits mask_data in place.
|
|
|
|
Requires open-cv as a dependency.
|
|
"""
|
|
if len(mask_data["rles"]) == 0:
|
|
return mask_data
|
|
|
|
|
|
new_masks = []
|
|
scores = []
|
|
for rle in mask_data["rles"]:
|
|
mask = rle_to_mask(rle)
|
|
|
|
mask, changed = remove_small_regions(mask, min_area, mode="holes")
|
|
unchanged = not changed
|
|
mask, changed = remove_small_regions(mask, min_area, mode="islands")
|
|
unchanged = unchanged and not changed
|
|
|
|
new_masks.append(torch.as_tensor(mask).unsqueeze(0))
|
|
|
|
|
|
scores.append(float(unchanged))
|
|
|
|
|
|
masks = torch.cat(new_masks, dim=0)
|
|
boxes = batched_mask_to_box(masks)
|
|
keep_by_nms = batched_nms(
|
|
boxes.float(),
|
|
torch.as_tensor(scores),
|
|
torch.zeros_like(boxes[:, 0]),
|
|
iou_threshold=nms_thresh,
|
|
)
|
|
|
|
|
|
for i_mask in keep_by_nms:
|
|
if scores[i_mask] == 0.0:
|
|
mask_torch = masks[i_mask].unsqueeze(0)
|
|
mask_data["rles"][i_mask] = mask_to_rle_pytorch(mask_torch)[0]
|
|
mask_data["boxes"][i_mask] = boxes[i_mask]
|
|
mask_data.filter(keep_by_nms)
|
|
|
|
return mask_data
|
|
|
|
def refine_with_m2m(self, points, point_labels, low_res_masks, points_per_batch):
|
|
new_masks = []
|
|
new_iou_preds = []
|
|
|
|
for cur_points, cur_point_labels, low_res_mask in batch_iterator(
|
|
points_per_batch, points, point_labels, low_res_masks
|
|
):
|
|
best_masks, best_iou_preds, _ = self.predictor._predict(
|
|
cur_points[:, None, :],
|
|
cur_point_labels[:, None],
|
|
mask_input=low_res_mask[:, None, :],
|
|
multimask_output=False,
|
|
return_logits=True,
|
|
)
|
|
new_masks.append(best_masks)
|
|
new_iou_preds.append(best_iou_preds)
|
|
masks = torch.cat(new_masks, dim=0)
|
|
return masks, torch.cat(new_iou_preds, dim=0)
|
|
|