CoolFace
Apppublic

yslan/ObjCtrl-2.5D

sourceHugging Faceapache-2.0updated 2y agoView on Hugging Face
10likes
misc.py350 linesDownload Raw Back to utils
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 os8import warnings9from threading import Thread10 11import numpy as np12import torch13from PIL import Image14from tqdm import tqdm15 16 17def get_sdpa_settings():18    if torch.cuda.is_available():19        old_gpu = torch.cuda.get_device_properties(0).major < 720        # only use Flash Attention on Ampere (8.0) or newer GPUs21        use_flash_attn = torch.cuda.get_device_properties(0).major >= 822        if not use_flash_attn:23            warnings.warn(24                "Flash Attention is disabled as it requires a GPU with Ampere (8.0) CUDA capability.",25                category=UserWarning,26                stacklevel=2,27            )28        # keep math kernel for PyTorch versions before 2.2 (Flash Attention v2 is only29        # available on PyTorch 2.2+, while Flash Attention v1 cannot handle all cases)30        pytorch_version = tuple(int(v) for v in torch.__version__.split(".")[:2])31        if pytorch_version < (2, 2):32            warnings.warn(33                f"You are using PyTorch {torch.__version__} without Flash Attention v2 support. "34                "Consider upgrading to PyTorch 2.2+ for Flash Attention v2 (which could be faster).",35                category=UserWarning,36                stacklevel=2,37            )38        math_kernel_on = pytorch_version < (2, 2) or not use_flash_attn39    else:40        old_gpu = True41        use_flash_attn = False42        math_kernel_on = True43 44    return old_gpu, use_flash_attn, math_kernel_on45 46 47def get_connected_components(mask):48    """49    Get the connected components (8-connectivity) of binary masks of shape (N, 1, H, W).50 51    Inputs:52    - mask: A binary mask tensor of shape (N, 1, H, W), where 1 is foreground and 0 is53            background.54 55    Outputs:56    - labels: A tensor of shape (N, 1, H, W) containing the connected component labels57              for foreground pixels and 0 for background pixels.58    - counts: A tensor of shape (N, 1, H, W) containing the area of the connected59              components for foreground pixels and 0 for background pixels.60    """61    from sam2 import _C62 63    return _C.get_connected_componnets(mask.to(torch.uint8).contiguous())64 65 66def mask_to_box(masks: torch.Tensor):67    """68    compute bounding box given an input mask69 70    Inputs:71    - masks: [B, 1, H, W] masks, dtype=torch.Tensor72 73    Returns:74    - box_coords: [B, 1, 4], contains (x, y) coordinates of top left and bottom right box corners, dtype=torch.Tensor75    """76    B, _, h, w = masks.shape77    device = masks.device78    xs = torch.arange(w, device=device, dtype=torch.int32)79    ys = torch.arange(h, device=device, dtype=torch.int32)80    grid_xs, grid_ys = torch.meshgrid(xs, ys, indexing="xy")81    grid_xs = grid_xs[None, None, ...].expand(B, 1, h, w)82    grid_ys = grid_ys[None, None, ...].expand(B, 1, h, w)83    min_xs, _ = torch.min(torch.where(masks, grid_xs, w).flatten(-2), dim=-1)84    max_xs, _ = torch.max(torch.where(masks, grid_xs, -1).flatten(-2), dim=-1)85    min_ys, _ = torch.min(torch.where(masks, grid_ys, h).flatten(-2), dim=-1)86    max_ys, _ = torch.max(torch.where(masks, grid_ys, -1).flatten(-2), dim=-1)87    bbox_coords = torch.stack((min_xs, min_ys, max_xs, max_ys), dim=-1)88 89    return bbox_coords90 91 92def _load_img_as_tensor(img_path, image_size):93    img_pil = Image.open(img_path)94    img_np = np.array(img_pil.convert("RGB").resize((image_size, image_size)))95    if img_np.dtype == np.uint8:  # np.uint8 is expected for JPEG images96        img_np = img_np / 255.097    else:98        raise RuntimeError(f"Unknown image dtype: {img_np.dtype} on {img_path}")99    img = torch.from_numpy(img_np).permute(2, 0, 1)100    video_width, video_height = img_pil.size  # the original video size101    return img, video_height, video_width102 103 104class AsyncVideoFrameLoader:105    """106    A list of video frames to be load asynchronously without blocking session start.107    """108 109    def __init__(110        self,111        img_paths,112        image_size,113        offload_video_to_cpu,114        img_mean,115        img_std,116        compute_device,117    ):118        self.img_paths = img_paths119        self.image_size = image_size120        self.offload_video_to_cpu = offload_video_to_cpu121        self.img_mean = img_mean122        self.img_std = img_std123        # items in `self.images` will be loaded asynchronously124        self.images = [None] * len(img_paths)125        # catch and raise any exceptions in the async loading thread126        self.exception = None127        # video_height and video_width be filled when loading the first image128        self.video_height = None129        self.video_width = None130        self.compute_device = compute_device131 132        # load the first frame to fill video_height and video_width and also133        # to cache it (since it's most likely where the user will click)134        self.__getitem__(0)135 136        # load the rest of frames asynchronously without blocking the session start137        def _load_frames():138            try:139                for n in tqdm(range(len(self.images)), desc="frame loading (JPEG)"):140                    self.__getitem__(n)141            except Exception as e:142                self.exception = e143 144        self.thread = Thread(target=_load_frames, daemon=True)145        self.thread.start()146 147    def __getitem__(self, index):148        if self.exception is not None:149            raise RuntimeError("Failure in frame loading thread") from self.exception150 151        img = self.images[index]152        if img is not None:153            return img154 155        img, video_height, video_width = _load_img_as_tensor(156            self.img_paths[index], self.image_size157        )158        self.video_height = video_height159        self.video_width = video_width160        # normalize by mean and std161        img -= self.img_mean162        img /= self.img_std163        if not self.offload_video_to_cpu:164            img = img.to(self.compute_device, non_blocking=True)165        self.images[index] = img166        return img167 168    def __len__(self):169        return len(self.images)170 171 172def load_video_frames(173    video_path,174    image_size,175    offload_video_to_cpu,176    img_mean=(0.485, 0.456, 0.406),177    img_std=(0.229, 0.224, 0.225),178    async_loading_frames=False,179    compute_device=torch.device("cuda"),180):181    """182    Load the video frames from video_path. The frames are resized to image_size as in183    the model and are loaded to GPU if offload_video_to_cpu=False. This is used by the demo.184    """185    is_bytes = isinstance(video_path, bytes)186    is_str = isinstance(video_path, str)187    is_mp4_path = is_str and os.path.splitext(video_path)[-1] in [".mp4", ".MP4"]188    if is_bytes or is_mp4_path:189        return load_video_frames_from_video_file(190            video_path=video_path,191            image_size=image_size,192            offload_video_to_cpu=offload_video_to_cpu,193            img_mean=img_mean,194            img_std=img_std,195            compute_device=compute_device,196        )197    elif is_str and os.path.isdir(video_path):198        return load_video_frames_from_jpg_images(199            video_path=video_path,200            image_size=image_size,201            offload_video_to_cpu=offload_video_to_cpu,202            img_mean=img_mean,203            img_std=img_std,204            async_loading_frames=async_loading_frames,205            compute_device=compute_device,206        )207    else:208        raise NotImplementedError(209            "Only MP4 video and JPEG folder are supported at this moment"210        )211 212 213def load_video_frames_from_jpg_images(214    video_path,215    image_size,216    offload_video_to_cpu,217    img_mean=(0.485, 0.456, 0.406),218    img_std=(0.229, 0.224, 0.225),219    async_loading_frames=False,220    compute_device=torch.device("cuda"),221):222    """223    Load the video frames from a directory of JPEG files ("<frame_index>.jpg" format).224 225    The frames are resized to image_size x image_size and are loaded to GPU if226    `offload_video_to_cpu` is `False` and to CPU if `offload_video_to_cpu` is `True`.227 228    You can load a frame asynchronously by setting `async_loading_frames` to `True`.229    """230    if isinstance(video_path, str) and os.path.isdir(video_path):231        jpg_folder = video_path232    else:233        raise NotImplementedError(234            "Only JPEG frames are supported at this moment. For video files, you may use "235            "ffmpeg (https://ffmpeg.org/) to extract frames into a folder of JPEG files, such as \n"236            "```\n"237            "ffmpeg -i <your_video>.mp4 -q:v 2 -start_number 0 <output_dir>/'%05d.jpg'\n"238            "```\n"239            "where `-q:v` generates high-quality JPEG frames and `-start_number 0` asks "240            "ffmpeg to start the JPEG file from 00000.jpg."241        )242 243    frame_names = [244        p245        for p in os.listdir(jpg_folder)246        if os.path.splitext(p)[-1] in [".jpg", ".jpeg", ".JPG", ".JPEG"]247    ]248    frame_names.sort(key=lambda p: int(os.path.splitext(p)[0]))249    num_frames = len(frame_names)250    if num_frames == 0:251        raise RuntimeError(f"no images found in {jpg_folder}")252    img_paths = [os.path.join(jpg_folder, frame_name) for frame_name in frame_names]253    img_mean = torch.tensor(img_mean, dtype=torch.float32)[:, None, None]254    img_std = torch.tensor(img_std, dtype=torch.float32)[:, None, None]255 256    if async_loading_frames:257        lazy_images = AsyncVideoFrameLoader(258            img_paths,259            image_size,260            offload_video_to_cpu,261            img_mean,262            img_std,263            compute_device,264        )265        return lazy_images, lazy_images.video_height, lazy_images.video_width266 267    images = torch.zeros(num_frames, 3, image_size, image_size, dtype=torch.float32)268    for n, img_path in enumerate(tqdm(img_paths, desc="frame loading (JPEG)")):269        images[n], video_height, video_width = _load_img_as_tensor(img_path, image_size)270    if not offload_video_to_cpu:271        images = images.to(compute_device)272        img_mean = img_mean.to(compute_device)273        img_std = img_std.to(compute_device)274    # normalize by mean and std275    images -= img_mean276    images /= img_std277    return images, video_height, video_width278 279 280def load_video_frames_from_video_file(281    video_path,282    image_size,283    offload_video_to_cpu,284    img_mean=(0.485, 0.456, 0.406),285    img_std=(0.229, 0.224, 0.225),286    compute_device=torch.device("cuda"),287):288    """Load the video frames from a video file."""289    import decord290 291    img_mean = torch.tensor(img_mean, dtype=torch.float32)[:, None, None]292    img_std = torch.tensor(img_std, dtype=torch.float32)[:, None, None]293    # Get the original video height and width294    decord.bridge.set_bridge("torch")295    video_height, video_width, _ = decord.VideoReader(video_path).next().shape296    # Iterate over all frames in the video297    images = []298    for frame in decord.VideoReader(video_path, width=image_size, height=image_size):299        images.append(frame.permute(2, 0, 1))300 301    images = torch.stack(images, dim=0).float() / 255.0302    if not offload_video_to_cpu:303        images = images.to(compute_device)304        img_mean = img_mean.to(compute_device)305        img_std = img_std.to(compute_device)306    # normalize by mean and std307    images -= img_mean308    images /= img_std309    return images, video_height, video_width310 311 312def fill_holes_in_mask_scores(mask, max_area):313    """314    A post processor to fill small holes in mask scores with area under `max_area`.315    """316    # Holes are those connected components in background with area <= self.max_area317    # (background regions are those with mask scores <= 0)318    assert max_area > 0, "max_area must be positive"319 320    input_mask = mask321    try:322        labels, areas = get_connected_components(mask <= 0)323        is_hole = (labels > 0) & (areas <= max_area)324        # We fill holes with a small positive mask score (0.1) to change them to foreground.325        mask = torch.where(is_hole, 0.1, mask)326    except Exception as e:327        # Skip the post-processing step on removing small holes if the CUDA kernel fails328        warnings.warn(329            f"{e}\n\nSkipping the post-processing step due to the error above. You can "330            "still use SAM 2 and it's OK to ignore the error above, although some post-processing "331            "functionality may be limited (which doesn't affect the results in most cases; see "332            "https://github.com/facebookresearch/sam2/blob/main/INSTALL.md).",333            category=UserWarning,334            stacklevel=2,335        )336        mask = input_mask337 338    return mask339 340 341def concat_points(old_point_inputs, new_points, new_labels):342    """Add new points and labels to previous point inputs (add at the end)."""343    if old_point_inputs is None:344        points, labels = new_points, new_labels345    else:346        points = torch.cat([old_point_inputs["point_coords"], new_points], dim=1)347        labels = torch.cat([old_point_inputs["point_labels"], new_labels], dim=1)348 349    return {"point_coords": points, "point_labels": labels}350