CoolFace
Apppublic

ZiyuG/SAM2Point

sourceHugging Faceapache-2.0updated 2y agoView on Hugging Face
16likes
sam2_video_predictor.py958 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 7import warnings8from collections import OrderedDict9 10import torch11 12from tqdm import tqdm13 14from sam2.modeling.sam2_base import NO_OBJ_SCORE, SAM2Base15from sam2.utils.misc import concat_points, fill_holes_in_mask_scores, load_video_frames16 17 18class SAM2VideoPredictor(SAM2Base):19    """The predictor class to handle user interactions and manage inference states."""20 21    def __init__(22        self,23        fill_hole_area=0,24        # whether to apply non-overlapping constraints on the output object masks25        non_overlap_masks=False,26        # whether to clear non-conditioning memory of the surrounding frames (which may contain outdated information) after adding correction clicks;27        # note that this would only apply to *single-object tracking* unless `clear_non_cond_mem_for_multi_obj` is also set to True)28        clear_non_cond_mem_around_input=False,29        # whether to also clear non-conditioning memory of the surrounding frames (only effective when `clear_non_cond_mem_around_input` is True).30        clear_non_cond_mem_for_multi_obj=False,31        **kwargs,32    ):33        super().__init__(**kwargs)34        self.fill_hole_area = fill_hole_area35        self.non_overlap_masks = non_overlap_masks36        self.clear_non_cond_mem_around_input = clear_non_cond_mem_around_input37        self.clear_non_cond_mem_for_multi_obj = clear_non_cond_mem_for_multi_obj38 39    @torch.inference_mode()40    def init_state(41        self,42        frame_paths,43        offload_video_to_cpu=False,44        offload_state_to_cpu=False,45        async_loading_frames=False,46    ):47        """Initialize a inference state."""48        images, video_height, video_width = load_video_frames(49            img_paths=frame_paths,50            image_size=self.image_size,51            offload_video_to_cpu=offload_video_to_cpu,52            async_loading_frames=async_loading_frames,53        )54        inference_state = {}55        inference_state["images"] = images56        inference_state["num_frames"] = len(images)57        # whether to offload the video frames to CPU memory58        # turning on this option saves the GPU memory with only a very small overhead59        inference_state["offload_video_to_cpu"] = offload_video_to_cpu60        # whether to offload the inference state to CPU memory61        # turning on this option saves the GPU memory at the cost of a lower tracking fps62        # (e.g. in a test case of 768x768 model, fps dropped from 27 to 24 when tracking one object63        # and from 24 to 21 when tracking two objects)64        inference_state["offload_state_to_cpu"] = offload_state_to_cpu65        # the original video height and width, used for resizing final output scores66        inference_state["video_height"] = video_height67        inference_state["video_width"] = video_width68        inference_state["device"] = torch.device("cuda")69        if offload_state_to_cpu:70            inference_state["storage_device"] = torch.device("cpu")71        else:72            inference_state["storage_device"] = torch.device("cuda")73        # inputs on each frame74        inference_state["point_inputs_per_obj"] = {}75        inference_state["mask_inputs_per_obj"] = {}76        # visual features on a small number of recently visited frames for quick interactions77        inference_state["cached_features"] = {}78        # values that don't change across frames (so we only need to hold one copy of them)79        inference_state["constants"] = {}80        # mapping between client-side object id and model-side object index81        inference_state["obj_id_to_idx"] = OrderedDict()82        inference_state["obj_idx_to_id"] = OrderedDict()83        inference_state["obj_ids"] = []84        # A storage to hold the model's tracking results and states on each frame85        inference_state["output_dict"] = {86            "cond_frame_outputs": {},  # dict containing {frame_idx: <out>}87            "non_cond_frame_outputs": {},  # dict containing {frame_idx: <out>}88        }89        # Slice (view) of each object tracking results, sharing the same memory with "output_dict"90        inference_state["output_dict_per_obj"] = {}91        # A temporary storage to hold new outputs when user interact with a frame92        # to add clicks or mask (it's merged into "output_dict" before propagation starts)93        inference_state["temp_output_dict_per_obj"] = {}94        # Frames that already holds consolidated outputs from click or mask inputs95        # (we directly use their consolidated outputs during tracking)96        inference_state["consolidated_frame_inds"] = {97            "cond_frame_outputs": set(),  # set containing frame indices98            "non_cond_frame_outputs": set(),  # set containing frame indices99        }100        # metadata for each tracking frame (e.g. which direction it's tracked)101        inference_state["tracking_has_started"] = False102        inference_state["frames_already_tracked"] = {}103        # Warm up the visual backbone and cache the image feature on frame 0104        self._get_image_feature(inference_state, frame_idx=0, batch_size=1)105        return inference_state106 107    @classmethod108    def from_pretrained(cls, model_id: str, **kwargs) -> "SAM2VideoPredictor":109        """110        Load a pretrained model from the Hugging Face hub.111 112        Arguments:113          model_id (str): The Hugging Face repository ID.114          **kwargs: Additional arguments to pass to the model constructor.115 116        Returns:117          (SAM2VideoPredictor): The loaded model.118        """119        from sam2.build_sam import build_sam2_video_predictor_hf120 121        sam_model = build_sam2_video_predictor_hf(model_id, **kwargs)122        return cls(sam_model)123 124    def _obj_id_to_idx(self, inference_state, obj_id):125        """Map client-side object id to model-side object index."""126        obj_idx = inference_state["obj_id_to_idx"].get(obj_id, None)127        if obj_idx is not None:128            return obj_idx129 130        # This is a new object id not sent to the server before. We only allow adding131        # new objects *before* the tracking starts.132        allow_new_object = not inference_state["tracking_has_started"]133        if allow_new_object:134            # get the next object slot135            obj_idx = len(inference_state["obj_id_to_idx"])136            inference_state["obj_id_to_idx"][obj_id] = obj_idx137            inference_state["obj_idx_to_id"][obj_idx] = obj_id138            inference_state["obj_ids"] = list(inference_state["obj_id_to_idx"])139            # set up input and output structures for this object140            inference_state["point_inputs_per_obj"][obj_idx] = {}141            inference_state["mask_inputs_per_obj"][obj_idx] = {}142            inference_state["output_dict_per_obj"][obj_idx] = {143                "cond_frame_outputs": {},  # dict containing {frame_idx: <out>}144                "non_cond_frame_outputs": {},  # dict containing {frame_idx: <out>}145            }146            inference_state["temp_output_dict_per_obj"][obj_idx] = {147                "cond_frame_outputs": {},  # dict containing {frame_idx: <out>}148                "non_cond_frame_outputs": {},  # dict containing {frame_idx: <out>}149            }150            return obj_idx151        else:152            raise RuntimeError(153                f"Cannot add new object id {obj_id} after tracking starts. "154                f"All existing object ids: {inference_state['obj_ids']}. "155                f"Please call 'reset_state' to restart from scratch."156            )157 158    def _obj_idx_to_id(self, inference_state, obj_idx):159        """Map model-side object index to client-side object id."""160        return inference_state["obj_idx_to_id"][obj_idx]161 162    def _get_obj_num(self, inference_state):163        """Get the total number of unique object ids received so far in this session."""164        return len(inference_state["obj_idx_to_id"])165 166    @torch.inference_mode()167    def add_new_points_or_box(168        self,169        inference_state,170        frame_idx,171        obj_id,172        points=None,173        labels=None,174        clear_old_points=True,175        normalize_coords=True,176        box=None,177    ):178        """Add new points to a frame."""179        obj_idx = self._obj_id_to_idx(inference_state, obj_id)180        point_inputs_per_frame = inference_state["point_inputs_per_obj"][obj_idx]181        mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx]182 183        if (points is not None) != (labels is not None):184            raise ValueError("points and labels must be provided together")185        if points is None and box is None:186            raise ValueError("at least one of points or box must be provided as input")187 188        if points is None:189            points = torch.zeros(0, 2, dtype=torch.float32)190        elif not isinstance(points, torch.Tensor):191            points = torch.tensor(points, dtype=torch.float32)192        if labels is None:193            labels = torch.zeros(0, dtype=torch.int32)194        elif not isinstance(labels, torch.Tensor):195            labels = torch.tensor(labels, dtype=torch.int32)196        if points.dim() == 2:197            points = points.unsqueeze(0)  # add batch dimension198        if labels.dim() == 1:199            labels = labels.unsqueeze(0)  # add batch dimension200 201        # If `box` is provided, we add it as the first two points with labels 2 and 3202        # along with the user-provided points (consistent with how SAM 2 is trained).203        if box is not None:204            if not clear_old_points:205                raise ValueError(206                    "cannot add box without clearing old points, since "207                    "box prompt must be provided before any point prompt "208                    "(please use clear_old_points=True instead)"209                )210            if inference_state["tracking_has_started"]:211                warnings.warn(212                    "You are adding a box after tracking starts. SAM 2 may not always be "213                    "able to incorporate a box prompt for *refinement*. If you intend to "214                    "use box prompt as an *initial* input before tracking, please call "215                    "'reset_state' on the inference state to restart from scratch.",216                    category=UserWarning,217                    stacklevel=2,218                )219            if not isinstance(box, torch.Tensor):220                box = torch.tensor(box, dtype=torch.float32, device=points.device)221            box_coords = box.reshape(1, 2, 2)222            box_labels = torch.tensor([2, 3], dtype=torch.int32, device=labels.device)223            box_labels = box_labels.reshape(1, 2)224            points = torch.cat([box_coords, points], dim=1)225            labels = torch.cat([box_labels, labels], dim=1)226 227        if normalize_coords:228            video_H = inference_state["video_height"]229            video_W = inference_state["video_width"]230            points = points / torch.tensor([video_W, video_H]).to(points.device)231        # scale the (normalized) coordinates by the model's internal image size232        points = points * self.image_size233        points = points.to(inference_state["device"])234        labels = labels.to(inference_state["device"])235 236        if not clear_old_points:237            point_inputs = point_inputs_per_frame.get(frame_idx, None)238        else:239            point_inputs = None240        point_inputs = concat_points(point_inputs, points, labels)241 242        point_inputs_per_frame[frame_idx] = point_inputs243        mask_inputs_per_frame.pop(frame_idx, None)244        # If this frame hasn't been tracked before, we treat it as an initial conditioning245        # frame, meaning that the inputs points are to generate segments on this frame without246        # using any memory from other frames, like in SAM. Otherwise (if it has been tracked),247        # the input points will be used to correct the already tracked masks.248        is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"]249        # whether to track in reverse time order250        if is_init_cond_frame:251            reverse = False252        else:253            reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"]254        obj_output_dict = inference_state["output_dict_per_obj"][obj_idx]255        obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx]256        # Add a frame to conditioning output if it's an initial conditioning frame or257        # if the model sees all frames receiving clicks/mask as conditioning frames.258        is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond259        storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs"260 261        # Get any previously predicted mask logits on this object and feed it along with262        # the new clicks into the SAM mask decoder.263        prev_sam_mask_logits = None264        # lookup temporary output dict first, which contains the most recent output265        # (if not found, then lookup conditioning and non-conditioning frame output)266        prev_out = obj_temp_output_dict[storage_key].get(frame_idx)267        if prev_out is None:268            prev_out = obj_output_dict["cond_frame_outputs"].get(frame_idx)269            if prev_out is None:270                prev_out = obj_output_dict["non_cond_frame_outputs"].get(frame_idx)271 272        if prev_out is not None and prev_out["pred_masks"] is not None:273            prev_sam_mask_logits = prev_out["pred_masks"].cuda(non_blocking=True)274            # Clamp the scale of prev_sam_mask_logits to avoid rare numerical issues.275            prev_sam_mask_logits = torch.clamp(prev_sam_mask_logits, -32.0, 32.0)276        current_out, _ = self._run_single_frame_inference(277            inference_state=inference_state,278            output_dict=obj_output_dict,  # run on the slice of a single object279            frame_idx=frame_idx,280            batch_size=1,  # run on the slice of a single object281            is_init_cond_frame=is_init_cond_frame,282            point_inputs=point_inputs,283            mask_inputs=None,284            reverse=reverse,285            # Skip the memory encoder when adding clicks or mask. We execute the memory encoder286            # at the beginning of `propagate_in_video` (after user finalize their clicks). This287            # allows us to enforce non-overlapping constraints on all objects before encoding288            # them into memory.289            run_mem_encoder=False,290            prev_sam_mask_logits=prev_sam_mask_logits,291        )292        # Add the output to the output dict (to be used as future memory)293        obj_temp_output_dict[storage_key][frame_idx] = current_out294 295        # Resize the output mask to the original video resolution296        obj_ids = inference_state["obj_ids"]297        consolidated_out = self._consolidate_temp_output_across_obj(298            inference_state,299            frame_idx,300            is_cond=is_cond,301            run_mem_encoder=False,302            consolidate_at_video_res=True,303        )304        _, video_res_masks = self._get_orig_video_res_output(305            inference_state, consolidated_out["pred_masks_video_res"]306        )307        return frame_idx, obj_ids, video_res_masks308 309    def add_new_points(self, *args, **kwargs):310        """Deprecated method. Please use `add_new_points_or_box` instead."""311        return self.add_new_points_or_box(*args, **kwargs)312 313    @torch.inference_mode()314    def add_new_mask(315        self,316        inference_state,317        frame_idx,318        obj_id,319        mask,320    ):321        """Add new mask to a frame."""322        obj_idx = self._obj_id_to_idx(inference_state, obj_id)323        point_inputs_per_frame = inference_state["point_inputs_per_obj"][obj_idx]324        mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx]325 326        if not isinstance(mask, torch.Tensor):327            mask = torch.tensor(mask, dtype=torch.bool)328        assert mask.dim() == 2329        mask_H, mask_W = mask.shape330        mask_inputs_orig = mask[None, None]  # add batch and channel dimension331        mask_inputs_orig = mask_inputs_orig.float().to(inference_state["device"])332 333        # resize the mask if it doesn't match the model's image size334        if mask_H != self.image_size or mask_W != self.image_size:335            mask_inputs = torch.nn.functional.interpolate(336                mask_inputs_orig,337                size=(self.image_size, self.image_size),338                align_corners=False,339                mode="bilinear",340                antialias=True,  # use antialias for downsampling341            )342            mask_inputs = (mask_inputs >= 0.5).float()343        else:344            mask_inputs = mask_inputs_orig345 346        mask_inputs_per_frame[frame_idx] = mask_inputs347        point_inputs_per_frame.pop(frame_idx, None)348        # If this frame hasn't been tracked before, we treat it as an initial conditioning349        # frame, meaning that the inputs points are to generate segments on this frame without350        # using any memory from other frames, like in SAM. Otherwise (if it has been tracked),351        # the input points will be used to correct the already tracked masks.352        is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"]353        # whether to track in reverse time order354        if is_init_cond_frame:355            reverse = False356        else:357            reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"]358        obj_output_dict = inference_state["output_dict_per_obj"][obj_idx]359        obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx]360        # Add a frame to conditioning output if it's an initial conditioning frame or361        # if the model sees all frames receiving clicks/mask as conditioning frames.362        is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond363        storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs"364 365        current_out, _ = self._run_single_frame_inference(366            inference_state=inference_state,367            output_dict=obj_output_dict,  # run on the slice of a single object368            frame_idx=frame_idx,369            batch_size=1,  # run on the slice of a single object370            is_init_cond_frame=is_init_cond_frame,371            point_inputs=None,372            mask_inputs=mask_inputs,373            reverse=reverse,374            # Skip the memory encoder when adding clicks or mask. We execute the memory encoder375            # at the beginning of `propagate_in_video` (after user finalize their clicks). This376            # allows us to enforce non-overlapping constraints on all objects before encoding377            # them into memory.378            run_mem_encoder=False,379        )380        # Add the output to the output dict (to be used as future memory)381        obj_temp_output_dict[storage_key][frame_idx] = current_out382 383        # Resize the output mask to the original video resolution384        obj_ids = inference_state["obj_ids"]385        consolidated_out = self._consolidate_temp_output_across_obj(386            inference_state,387            frame_idx,388            is_cond=is_cond,389            run_mem_encoder=False,390            consolidate_at_video_res=True,391        )392        _, video_res_masks = self._get_orig_video_res_output(393            inference_state, consolidated_out["pred_masks_video_res"]394        )395        return frame_idx, obj_ids, video_res_masks396 397    def _get_orig_video_res_output(self, inference_state, any_res_masks):398        """399        Resize the object scores to the original video resolution (video_res_masks)400        and apply non-overlapping constraints for final output.401        """402        device = inference_state["device"]403        video_H = inference_state["video_height"]404        video_W = inference_state["video_width"]405        any_res_masks = any_res_masks.to(device, non_blocking=True)406        if any_res_masks.shape[-2:] == (video_H, video_W):407            video_res_masks = any_res_masks408        else:409            video_res_masks = torch.nn.functional.interpolate(410                any_res_masks,411                size=(video_H, video_W),412                mode="bilinear",413                align_corners=False,414            )415        if self.non_overlap_masks:416            video_res_masks = self._apply_non_overlapping_constraints(video_res_masks)417        return any_res_masks, video_res_masks418 419    def _consolidate_temp_output_across_obj(420        self,421        inference_state,422        frame_idx,423        is_cond,424        run_mem_encoder,425        consolidate_at_video_res=False,426    ):427        """428        Consolidate the per-object temporary outputs in `temp_output_dict_per_obj` on429        a frame into a single output for all objects, including430        1) fill any missing objects either from `output_dict_per_obj` (if they exist in431           `output_dict_per_obj` for this frame) or leave them as placeholder values432           (if they don't exist in `output_dict_per_obj` for this frame);433        2) if specified, rerun memory encoder after apply non-overlapping constraints434           on the object scores.435        """436        batch_size = self._get_obj_num(inference_state)437        storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs"438        # Optionally, we allow consolidating the temporary outputs at the original439        # video resolution (to provide a better editing experience for mask prompts).440        if consolidate_at_video_res:441            assert not run_mem_encoder, "memory encoder cannot run at video resolution"442            consolidated_H = inference_state["video_height"]443            consolidated_W = inference_state["video_width"]444            consolidated_mask_key = "pred_masks_video_res"445        else:446            consolidated_H = consolidated_W = self.image_size // 4447            consolidated_mask_key = "pred_masks"448 449        # Initialize `consolidated_out`. Its "maskmem_features" and "maskmem_pos_enc"450        # will be added when rerunning the memory encoder after applying non-overlapping451        # constraints to object scores. Its "pred_masks" are prefilled with a large452        # negative value (NO_OBJ_SCORE) to represent missing objects.453        consolidated_out = {454            "maskmem_features": None,455            "maskmem_pos_enc": None,456            consolidated_mask_key: torch.full(457                size=(batch_size, 1, consolidated_H, consolidated_W),458                fill_value=NO_OBJ_SCORE,459                dtype=torch.float32,460                device=inference_state["storage_device"],461            ),462            "obj_ptr": torch.full(463                size=(batch_size, self.hidden_dim),464                fill_value=NO_OBJ_SCORE,465                dtype=torch.float32,466                device=inference_state["device"],467            ),468        }469        empty_mask_ptr = None470        for obj_idx in range(batch_size):471            obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx]472            obj_output_dict = inference_state["output_dict_per_obj"][obj_idx]473            out = obj_temp_output_dict[storage_key].get(frame_idx, None)474            # If the object doesn't appear in "temp_output_dict_per_obj" on this frame,475            # we fall back and look up its previous output in "output_dict_per_obj".476            # We look up both "cond_frame_outputs" and "non_cond_frame_outputs" in477            # "output_dict_per_obj" to find a previous output for this object.478            if out is None:479                out = obj_output_dict["cond_frame_outputs"].get(frame_idx, None)480            if out is None:481                out = obj_output_dict["non_cond_frame_outputs"].get(frame_idx, None)482            # If the object doesn't appear in "output_dict_per_obj" either, we skip it483            # and leave its mask scores to the default scores (i.e. the NO_OBJ_SCORE484            # placeholder above) and set its object pointer to be a dummy pointer.485            if out is None:486                # Fill in dummy object pointers for those objects without any inputs or487                # tracking outcomes on this frame (only do it under `run_mem_encoder=True`,488                # i.e. when we need to build the memory for tracking).489                if run_mem_encoder:490                    if empty_mask_ptr is None:491                        empty_mask_ptr = self._get_empty_mask_ptr(492                            inference_state, frame_idx493                        )494                    # fill object pointer with a dummy pointer (based on an empty mask)495                    consolidated_out["obj_ptr"][obj_idx : obj_idx + 1] = empty_mask_ptr496                continue497            # Add the temporary object output mask to consolidated output mask498            obj_mask = out["pred_masks"]499            consolidated_pred_masks = consolidated_out[consolidated_mask_key]500            if obj_mask.shape[-2:] == consolidated_pred_masks.shape[-2:]:501                consolidated_pred_masks[obj_idx : obj_idx + 1] = obj_mask502            else:503                # Resize first if temporary object mask has a different resolution504                resized_obj_mask = torch.nn.functional.interpolate(505                    obj_mask,506                    size=consolidated_pred_masks.shape[-2:],507                    mode="bilinear",508                    align_corners=False,509                )510                consolidated_pred_masks[obj_idx : obj_idx + 1] = resized_obj_mask511            consolidated_out["obj_ptr"][obj_idx : obj_idx + 1] = out["obj_ptr"]512 513        # Optionally, apply non-overlapping constraints on the consolidated scores514        # and rerun the memory encoder515        if run_mem_encoder:516            device = inference_state["device"]517            high_res_masks = torch.nn.functional.interpolate(518                consolidated_out["pred_masks"].to(device, non_blocking=True),519                size=(self.image_size, self.image_size),520                mode="bilinear",521                align_corners=False,522            )523            if self.non_overlap_masks_for_mem_enc:524                high_res_masks = self._apply_non_overlapping_constraints(high_res_masks)525            maskmem_features, maskmem_pos_enc = self._run_memory_encoder(526                inference_state=inference_state,527                frame_idx=frame_idx,528                batch_size=batch_size,529                high_res_masks=high_res_masks,530                is_mask_from_pts=True,  # these frames are what the user interacted with531            )532            consolidated_out["maskmem_features"] = maskmem_features533            consolidated_out["maskmem_pos_enc"] = maskmem_pos_enc534 535        return consolidated_out536 537    def _get_empty_mask_ptr(self, inference_state, frame_idx):538        """Get a dummy object pointer based on an empty mask on the current frame."""539        # A dummy (empty) mask with a single object540        batch_size = 1541        mask_inputs = torch.zeros(542            (batch_size, 1, self.image_size, self.image_size),543            dtype=torch.float32,544            device=inference_state["device"],545        )546 547        # Retrieve correct image features548        (549            _,550            _,551            current_vision_feats,552            current_vision_pos_embeds,553            feat_sizes,554        ) = self._get_image_feature(inference_state, frame_idx, batch_size)555 556        # Feed the empty mask and image feature above to get a dummy object pointer557        current_out = self.track_step(558            frame_idx=frame_idx,559            is_init_cond_frame=True,560            current_vision_feats=current_vision_feats,561            current_vision_pos_embeds=current_vision_pos_embeds,562            feat_sizes=feat_sizes,563            point_inputs=None,564            mask_inputs=mask_inputs,565            output_dict={},566            num_frames=inference_state["num_frames"],567            track_in_reverse=False,568            run_mem_encoder=False,569            prev_sam_mask_logits=None,570        )571        return current_out["obj_ptr"]572 573    @torch.inference_mode()574    def propagate_in_video_preflight(self, inference_state):575        """Prepare inference_state and consolidate temporary outputs before tracking."""576        # Tracking has started and we don't allow adding new objects until session is reset.577        inference_state["tracking_has_started"] = True578        batch_size = self._get_obj_num(inference_state)579 580        # Consolidate per-object temporary outputs in "temp_output_dict_per_obj" and581        # add them into "output_dict".582        temp_output_dict_per_obj = inference_state["temp_output_dict_per_obj"]583        output_dict = inference_state["output_dict"]584        # "consolidated_frame_inds" contains indices of those frames where consolidated585        # temporary outputs have been added (either in this call or any previous calls586        # to `propagate_in_video_preflight`).587        consolidated_frame_inds = inference_state["consolidated_frame_inds"]588        for is_cond in [False, True]:589            # Separately consolidate conditioning and non-conditioning temp outptus590            storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs"591            # Find all the frames that contain temporary outputs for any objects592            # (these should be the frames that have just received clicks for mask inputs593            # via `add_new_points_or_box` or `add_new_mask`)594            temp_frame_inds = set()595            for obj_temp_output_dict in temp_output_dict_per_obj.values():596                temp_frame_inds.update(obj_temp_output_dict[storage_key].keys())597            consolidated_frame_inds[storage_key].update(temp_frame_inds)598            # consolidate the temprary output across all objects on this frame599            for frame_idx in temp_frame_inds:600                consolidated_out = self._consolidate_temp_output_across_obj(601                    inference_state, frame_idx, is_cond=is_cond, run_mem_encoder=True602                )603                # merge them into "output_dict" and also create per-object slices604                output_dict[storage_key][frame_idx] = consolidated_out605                self._add_output_per_object(606                    inference_state, frame_idx, consolidated_out, storage_key607                )608                clear_non_cond_mem = self.clear_non_cond_mem_around_input and (609                    self.clear_non_cond_mem_for_multi_obj or batch_size <= 1610                )611                if clear_non_cond_mem:612                    # clear non-conditioning memory of the surrounding frames613                    self._clear_non_cond_mem_around_input(inference_state, frame_idx)614 615            # clear temporary outputs in `temp_output_dict_per_obj`616            for obj_temp_output_dict in temp_output_dict_per_obj.values():617                obj_temp_output_dict[storage_key].clear()618 619        # edge case: if an output is added to "cond_frame_outputs", we remove any prior620        # output on the same frame in "non_cond_frame_outputs"621        for frame_idx in output_dict["cond_frame_outputs"]:622            output_dict["non_cond_frame_outputs"].pop(frame_idx, None)623        for obj_output_dict in inference_state["output_dict_per_obj"].values():624            for frame_idx in obj_output_dict["cond_frame_outputs"]:625                obj_output_dict["non_cond_frame_outputs"].pop(frame_idx, None)626        for frame_idx in consolidated_frame_inds["cond_frame_outputs"]:627            assert frame_idx in output_dict["cond_frame_outputs"]628            consolidated_frame_inds["non_cond_frame_outputs"].discard(frame_idx)629 630        # Make sure that the frame indices in "consolidated_frame_inds" are exactly those frames631        # with either points or mask inputs (which should be true under a correct workflow).632        all_consolidated_frame_inds = (633            consolidated_frame_inds["cond_frame_outputs"]634            | consolidated_frame_inds["non_cond_frame_outputs"]635        )636        input_frames_inds = set()637        for point_inputs_per_frame in inference_state["point_inputs_per_obj"].values():638            input_frames_inds.update(point_inputs_per_frame.keys())639        for mask_inputs_per_frame in inference_state["mask_inputs_per_obj"].values():640            input_frames_inds.update(mask_inputs_per_frame.keys())641        assert all_consolidated_frame_inds == input_frames_inds642 643    @torch.inference_mode()644    def propagate_in_video(645        self,646        inference_state,647        start_frame_idx=None,648        max_frame_num_to_track=None,649        reverse=False,650    ):651        """Propagate the input points across frames to track in the entire video."""652        self.propagate_in_video_preflight(inference_state)653 654        output_dict = inference_state["output_dict"]655        consolidated_frame_inds = inference_state["consolidated_frame_inds"]656        obj_ids = inference_state["obj_ids"]657        num_frames = inference_state["num_frames"]658        batch_size = self._get_obj_num(inference_state)659        if len(output_dict["cond_frame_outputs"]) == 0:660            raise RuntimeError("No points are provided; please add points first")661        clear_non_cond_mem = self.clear_non_cond_mem_around_input and (662            self.clear_non_cond_mem_for_multi_obj or batch_size <= 1663        )664 665        # set start index, end index, and processing order666        if start_frame_idx is None:667            # default: start from the earliest frame with input points668            start_frame_idx = min(output_dict["cond_frame_outputs"])669        if max_frame_num_to_track is None:670            # default: track all the frames in the video671            max_frame_num_to_track = num_frames672        if reverse:673            end_frame_idx = max(start_frame_idx - max_frame_num_to_track, 0)674            if start_frame_idx > 0:675                processing_order = range(start_frame_idx, end_frame_idx - 1, -1)676            else:677                processing_order = []  # skip reverse tracking if starting from frame 0678        else:679            end_frame_idx = min(680                start_frame_idx + max_frame_num_to_track, num_frames - 1681            )682            processing_order = range(start_frame_idx, end_frame_idx + 1)683 684        for frame_idx in tqdm(processing_order, desc="propagate in video"):685            # We skip those frames already in consolidated outputs (these are frames686            # that received input clicks or mask). Note that we cannot directly run687            # batched forward on them via `_run_single_frame_inference` because the688            # number of clicks on each object might be different.689            if frame_idx in consolidated_frame_inds["cond_frame_outputs"]:690                storage_key = "cond_frame_outputs"691                current_out = output_dict[storage_key][frame_idx]692                pred_masks = current_out["pred_masks"]693                if clear_non_cond_mem:694                    # clear non-conditioning memory of the surrounding frames695                    self._clear_non_cond_mem_around_input(inference_state, frame_idx)696            elif frame_idx in consolidated_frame_inds["non_cond_frame_outputs"]:697                storage_key = "non_cond_frame_outputs"698                current_out = output_dict[storage_key][frame_idx]699                pred_masks = current_out["pred_masks"]700            else:701                storage_key = "non_cond_frame_outputs"702                current_out, pred_masks = self._run_single_frame_inference(703                    inference_state=inference_state,704                    output_dict=output_dict,705                    frame_idx=frame_idx,706                    batch_size=batch_size,707                    is_init_cond_frame=False,708                    point_inputs=None,709                    mask_inputs=None,710                    reverse=reverse,711                    run_mem_encoder=True,712                )713                output_dict[storage_key][frame_idx] = current_out714            # Create slices of per-object outputs for subsequent interaction with each715            # individual object after tracking.716            self._add_output_per_object(717                inference_state, frame_idx, current_out, storage_key718            )719            inference_state["frames_already_tracked"][frame_idx] = {"reverse": reverse}720 721            # Resize the output mask to the original video resolution (we directly use722            # the mask scores on GPU for output to avoid any CPU conversion in between)723            _, video_res_masks = self._get_orig_video_res_output(724                inference_state, pred_masks725            )726            yield frame_idx, obj_ids, video_res_masks727 728    def _add_output_per_object(729        self, inference_state, frame_idx, current_out, storage_key730    ):731        """732        Split a multi-object output into per-object output slices and add them into733        `output_dict_per_obj`. The resulting slices share the same tensor storage.734        """735        maskmem_features = current_out["maskmem_features"]736        assert maskmem_features is None or isinstance(maskmem_features, torch.Tensor)737 738        maskmem_pos_enc = current_out["maskmem_pos_enc"]739        assert maskmem_pos_enc is None or isinstance(maskmem_pos_enc, list)740 741        output_dict_per_obj = inference_state["output_dict_per_obj"]742        for obj_idx, obj_output_dict in output_dict_per_obj.items():743            obj_slice = slice(obj_idx, obj_idx + 1)744            obj_out = {745                "maskmem_features": None,746                "maskmem_pos_enc": None,747                "pred_masks": current_out["pred_masks"][obj_slice],748                "obj_ptr": current_out["obj_ptr"][obj_slice],749            }750            if maskmem_features is not None:751                obj_out["maskmem_features"] = maskmem_features[obj_slice]752            if maskmem_pos_enc is not None:753                obj_out["maskmem_pos_enc"] = [x[obj_slice] for x in maskmem_pos_enc]754            obj_output_dict[storage_key][frame_idx] = obj_out755 756    @torch.inference_mode()757    def reset_state(self, inference_state):758        """Remove all input points or mask in all frames throughout the video."""759        self._reset_tracking_results(inference_state)760        # Remove all object ids761        inference_state["obj_id_to_idx"].clear()762        inference_state["obj_idx_to_id"].clear()763        inference_state["obj_ids"].clear()764        inference_state["point_inputs_per_obj"].clear()765        inference_state["mask_inputs_per_obj"].clear()766        inference_state["output_dict_per_obj"].clear()767        inference_state["temp_output_dict_per_obj"].clear()768 769    def _reset_tracking_results(self, inference_state):770        """Reset all tracking inputs and results across the videos."""771        for v in inference_state["point_inputs_per_obj"].values():772            v.clear()773        for v in inference_state["mask_inputs_per_obj"].values():774            v.clear()775        for v in inference_state["output_dict_per_obj"].values():776            v["cond_frame_outputs"].clear()777            v["non_cond_frame_outputs"].clear()778        for v in inference_state["temp_output_dict_per_obj"].values():779            v["cond_frame_outputs"].clear()780            v["non_cond_frame_outputs"].clear()781        inference_state["output_dict"]["cond_frame_outputs"].clear()782        inference_state["output_dict"]["non_cond_frame_outputs"].clear()783        inference_state["consolidated_frame_inds"]["cond_frame_outputs"].clear()784        inference_state["consolidated_frame_inds"]["non_cond_frame_outputs"].clear()785        inference_state["tracking_has_started"] = False786        inference_state["frames_already_tracked"].clear()787 788    def _get_image_feature(self, inference_state, frame_idx, batch_size):789        """Compute the image features on a given frame."""790        # Look up in the cache first791        image, backbone_out = inference_state["cached_features"].get(792            frame_idx, (None, None)793        )794        if backbone_out is None:795            # Cache miss -- we will run inference on a single image796            image = inference_state["images"][frame_idx].cuda().float().unsqueeze(0)797            backbone_out = self.forward_image(image)798            # Cache the most recent frame's feature (for repeated interactions with799            # a frame; we can use an LRU cache for more frames in the future).800            inference_state["cached_features"] = {frame_idx: (image, backbone_out)}801 802        # expand the features to have the same dimension as the number of objects803        expanded_image = image.expand(batch_size, -1, -1, -1)804        expanded_backbone_out = {805            "backbone_fpn": backbone_out["backbone_fpn"].copy(),806            "vision_pos_enc": backbone_out["vision_pos_enc"].copy(),807        }808        for i, feat in enumerate(expanded_backbone_out["backbone_fpn"]):809            expanded_backbone_out["backbone_fpn"][i] = feat.expand(810                batch_size, -1, -1, -1811            )812        for i, pos in enumerate(expanded_backbone_out["vision_pos_enc"]):813            pos = pos.expand(batch_size, -1, -1, -1)814            expanded_backbone_out["vision_pos_enc"][i] = pos815 816        features = self._prepare_backbone_features(expanded_backbone_out)817        features = (expanded_image,) + features818        return features819 820    def _run_single_frame_inference(821        self,822        inference_state,823        output_dict,824        frame_idx,825        batch_size,826        is_init_cond_frame,827        point_inputs,828        mask_inputs,829        reverse,830        run_mem_encoder,831        prev_sam_mask_logits=None,832    ):833        """Run tracking on a single frame based on current inputs and previous memory."""834        # Retrieve correct image features835        (836            _,837            _,838            current_vision_feats,839            current_vision_pos_embeds,840            feat_sizes,841        ) = self._get_image_feature(inference_state, frame_idx, batch_size)842 843        # point and mask should not appear as input simultaneously on the same frame844        assert point_inputs is None or mask_inputs is None845        current_out = self.track_step(846            frame_idx=frame_idx,847            is_init_cond_frame=is_init_cond_frame,848            current_vision_feats=current_vision_feats,849            current_vision_pos_embeds=current_vision_pos_embeds,850            feat_sizes=feat_sizes,851            point_inputs=point_inputs,852            mask_inputs=mask_inputs,853            output_dict=output_dict,854            num_frames=inference_state["num_frames"],855            track_in_reverse=reverse,856            run_mem_encoder=run_mem_encoder,857            prev_sam_mask_logits=prev_sam_mask_logits,858        )859 860        # optionally offload the output to CPU memory to save GPU space861        storage_device = inference_state["storage_device"]862        maskmem_features = current_out["maskmem_features"]863        if maskmem_features is not None:864            maskmem_features = maskmem_features.to(torch.bfloat16)865            maskmem_features = maskmem_features.to(storage_device, non_blocking=True)866        pred_masks_gpu = current_out["pred_masks"]867        # potentially fill holes in the predicted masks868        if self.fill_hole_area > 0:869            pred_masks_gpu = fill_holes_in_mask_scores(870                pred_masks_gpu, self.fill_hole_area871            )872        pred_masks = pred_masks_gpu.to(storage_device, non_blocking=True)873        # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it874        maskmem_pos_enc = self._get_maskmem_pos_enc(inference_state, current_out)875        # object pointer is a small tensor, so we always keep it on GPU memory for fast access876        obj_ptr = current_out["obj_ptr"]877        # make a compact version of this frame's output to reduce the state size878        compact_current_out = {879            "maskmem_features": maskmem_features,880            "maskmem_pos_enc": maskmem_pos_enc,881            "pred_masks": pred_masks,882            "obj_ptr": obj_ptr,883        }884        return compact_current_out, pred_masks_gpu885 886    def _run_memory_encoder(887        self, inference_state, frame_idx, batch_size, high_res_masks, is_mask_from_pts888    ):889        """890        Run the memory encoder on `high_res_masks`. This is usually after applying891        non-overlapping constraints to object scores. Since their scores changed, their892        memory also need to be computed again with the memory encoder.893        """894        # Retrieve correct image features895        _, _, current_vision_feats, _, feat_sizes = self._get_image_feature(896            inference_state, frame_idx, batch_size897        )898        maskmem_features, maskmem_pos_enc = self._encode_new_memory(899            current_vision_feats=current_vision_feats,900            feat_sizes=feat_sizes,901            pred_masks_high_res=high_res_masks,902            is_mask_from_pts=is_mask_from_pts,903        )904 905        # optionally offload the output to CPU memory to save GPU space906        storage_device = inference_state["storage_device"]907        maskmem_features = maskmem_features.to(torch.bfloat16)908        maskmem_features = maskmem_features.to(storage_device, non_blocking=True)909        # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it910        maskmem_pos_enc = self._get_maskmem_pos_enc(911            inference_state, {"maskmem_pos_enc": maskmem_pos_enc}912        )913        return maskmem_features, maskmem_pos_enc914 915    def _get_maskmem_pos_enc(self, inference_state, current_out):916        """917        `maskmem_pos_enc` is the same across frames and objects, so we cache it as918        a constant in the inference session to reduce session storage size.919        """920        model_constants = inference_state["constants"]921        # "out_maskmem_pos_enc" should be either a list of tensors or None922        out_maskmem_pos_enc = current_out["maskmem_pos_enc"]923        if out_maskmem_pos_enc is not None:924            if "maskmem_pos_enc" not in model_constants:925                assert isinstance(out_maskmem_pos_enc, list)926                # only take the slice for one object, since it's same across objects927                maskmem_pos_enc = [x[0:1].clone() for x in out_maskmem_pos_enc]928                model_constants["maskmem_pos_enc"] = maskmem_pos_enc929            else:930                maskmem_pos_enc = model_constants["maskmem_pos_enc"]931            # expand the cached maskmem_pos_enc to the actual batch size932            batch_size = out_maskmem_pos_enc[0].size(0)933            expanded_maskmem_pos_enc = [934                x.expand(batch_size, -1, -1, -1) for x in maskmem_pos_enc935            ]936        else:937            expanded_maskmem_pos_enc = None938        return expanded_maskmem_pos_enc939 940    def _clear_non_cond_mem_around_input(self, inference_state, frame_idx):941        """942        Remove the non-conditioning memory around the input frame. When users provide943        correction clicks, the surrounding frames' non-conditioning memories can still944        contain outdated object appearance information and could confuse the model.945 946        This method clears those non-conditioning memories surrounding the interacted947        frame to avoid giving the model both old and new information about the object.948        """949        r = self.memory_temporal_stride_for_eval950        frame_idx_begin = frame_idx - r * self.num_maskmem951        frame_idx_end = frame_idx + r * self.num_maskmem952        output_dict = inference_state["output_dict"]953        non_cond_frame_outputs = output_dict["non_cond_frame_outputs"]954        for t in range(frame_idx_begin, frame_idx_end + 1):955            non_cond_frame_outputs.pop(t, None)956            for obj_output_dict in inference_state["output_dict_per_obj"].values():957                obj_output_dict["non_cond_frame_outputs"].pop(t, None)958