Team Ai
Apppublic

Arulkumar03/Wheat_HEAD_Detection_Counting_ComputerVision_Model

sourceHugging Facemitupdated 3y agoView on Hugging Face
0likes
masks.py535 linesDownload Raw Back to structures
1# Copyright (c) Facebook, Inc. and its affiliates.2import copy3import itertools4import numpy as np5from typing import Any, Iterator, List, Union6import pycocotools.mask as mask_util7import torch8from torch import device9 10from detectron2.layers.roi_align import ROIAlign11from detectron2.utils.memory import retry_if_cuda_oom12 13from .boxes import Boxes14 15 16def polygon_area(x, y):17    # Using the shoelace formula18    # https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates19    return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))20 21 22def polygons_to_bitmask(polygons: List[np.ndarray], height: int, width: int) -> np.ndarray:23    """24    Args:25        polygons (list[ndarray]): each array has shape (Nx2,)26        height, width (int)27 28    Returns:29        ndarray: a bool mask of shape (height, width)30    """31    if len(polygons) == 0:32        # COCOAPI does not support empty polygons33        return np.zeros((height, width)).astype(bool)34    rles = mask_util.frPyObjects(polygons, height, width)35    rle = mask_util.merge(rles)36    return mask_util.decode(rle).astype(bool)37 38 39def rasterize_polygons_within_box(40    polygons: List[np.ndarray], box: np.ndarray, mask_size: int41) -> torch.Tensor:42    """43    Rasterize the polygons into a mask image and44    crop the mask content in the given box.45    The cropped mask is resized to (mask_size, mask_size).46 47    This function is used when generating training targets for mask head in Mask R-CNN.48    Given original ground-truth masks for an image, new ground-truth mask49    training targets in the size of `mask_size x mask_size`50    must be provided for each predicted box. This function will be called to51    produce such targets.52 53    Args:54        polygons (list[ndarray[float]]): a list of polygons, which represents an instance.55        box: 4-element numpy array56        mask_size (int):57 58    Returns:59        Tensor: BoolTensor of shape (mask_size, mask_size)60    """61    # 1. Shift the polygons w.r.t the boxes62    w, h = box[2] - box[0], box[3] - box[1]63 64    polygons = copy.deepcopy(polygons)65    for p in polygons:66        p[0::2] = p[0::2] - box[0]67        p[1::2] = p[1::2] - box[1]68 69    # 2. Rescale the polygons to the new box size70    # max() to avoid division by small number71    ratio_h = mask_size / max(h, 0.1)72    ratio_w = mask_size / max(w, 0.1)73 74    if ratio_h == ratio_w:75        for p in polygons:76            p *= ratio_h77    else:78        for p in polygons:79            p[0::2] *= ratio_w80            p[1::2] *= ratio_h81 82    # 3. Rasterize the polygons with coco api83    mask = polygons_to_bitmask(polygons, mask_size, mask_size)84    mask = torch.from_numpy(mask)85    return mask86 87 88class BitMasks:89    """90    This class stores the segmentation masks for all objects in one image, in91    the form of bitmaps.92 93    Attributes:94        tensor: bool Tensor of N,H,W, representing N instances in the image.95    """96 97    def __init__(self, tensor: Union[torch.Tensor, np.ndarray]):98        """99        Args:100            tensor: bool Tensor of N,H,W, representing N instances in the image.101        """102        if isinstance(tensor, torch.Tensor):103            tensor = tensor.to(torch.bool)104        else:105            tensor = torch.as_tensor(tensor, dtype=torch.bool, device=torch.device("cpu"))106        assert tensor.dim() == 3, tensor.size()107        self.image_size = tensor.shape[1:]108        self.tensor = tensor109 110    @torch.jit.unused111    def to(self, *args: Any, **kwargs: Any) -> "BitMasks":112        return BitMasks(self.tensor.to(*args, **kwargs))113 114    @property115    def device(self) -> torch.device:116        return self.tensor.device117 118    @torch.jit.unused119    def __getitem__(self, item: Union[int, slice, torch.BoolTensor]) -> "BitMasks":120        """121        Returns:122            BitMasks: Create a new :class:`BitMasks` by indexing.123 124        The following usage are allowed:125 126        1. `new_masks = masks[3]`: return a `BitMasks` which contains only one mask.127        2. `new_masks = masks[2:10]`: return a slice of masks.128        3. `new_masks = masks[vector]`, where vector is a torch.BoolTensor129           with `length = len(masks)`. Nonzero elements in the vector will be selected.130 131        Note that the returned object might share storage with this object,132        subject to Pytorch's indexing semantics.133        """134        if isinstance(item, int):135            return BitMasks(self.tensor[item].unsqueeze(0))136        m = self.tensor[item]137        assert m.dim() == 3, "Indexing on BitMasks with {} returns a tensor with shape {}!".format(138            item, m.shape139        )140        return BitMasks(m)141 142    @torch.jit.unused143    def __iter__(self) -> torch.Tensor:144        yield from self.tensor145 146    @torch.jit.unused147    def __repr__(self) -> str:148        s = self.__class__.__name__ + "("149        s += "num_instances={})".format(len(self.tensor))150        return s151 152    def __len__(self) -> int:153        return self.tensor.shape[0]154 155    def nonempty(self) -> torch.Tensor:156        """157        Find masks that are non-empty.158 159        Returns:160            Tensor: a BoolTensor which represents161                whether each mask is empty (False) or non-empty (True).162        """163        return self.tensor.flatten(1).any(dim=1)164 165    @staticmethod166    def from_polygon_masks(167        polygon_masks: Union["PolygonMasks", List[List[np.ndarray]]], height: int, width: int168    ) -> "BitMasks":169        """170        Args:171            polygon_masks (list[list[ndarray]] or PolygonMasks)172            height, width (int)173        """174        if isinstance(polygon_masks, PolygonMasks):175            polygon_masks = polygon_masks.polygons176        masks = [polygons_to_bitmask(p, height, width) for p in polygon_masks]177        if len(masks):178            return BitMasks(torch.stack([torch.from_numpy(x) for x in masks]))179        else:180            return BitMasks(torch.empty(0, height, width, dtype=torch.bool))181 182    @staticmethod183    def from_roi_masks(roi_masks: "ROIMasks", height: int, width: int) -> "BitMasks":184        """185        Args:186            roi_masks:187            height, width (int):188        """189        return roi_masks.to_bitmasks(height, width)190 191    def crop_and_resize(self, boxes: torch.Tensor, mask_size: int) -> torch.Tensor:192        """193        Crop each bitmask by the given box, and resize results to (mask_size, mask_size).194        This can be used to prepare training targets for Mask R-CNN.195        It has less reconstruction error compared to rasterization with polygons.196        However we observe no difference in accuracy,197        but BitMasks requires more memory to store all the masks.198 199        Args:200            boxes (Tensor): Nx4 tensor storing the boxes for each mask201            mask_size (int): the size of the rasterized mask.202 203        Returns:204            Tensor:205                A bool tensor of shape (N, mask_size, mask_size), where206                N is the number of predicted boxes for this image.207        """208        assert len(boxes) == len(self), "{} != {}".format(len(boxes), len(self))209        device = self.tensor.device210 211        batch_inds = torch.arange(len(boxes), device=device).to(dtype=boxes.dtype)[:, None]212        rois = torch.cat([batch_inds, boxes], dim=1)  # Nx5213 214        bit_masks = self.tensor.to(dtype=torch.float32)215        rois = rois.to(device=device)216        output = (217            ROIAlign((mask_size, mask_size), 1.0, 0, aligned=True)218            .forward(bit_masks[:, None, :, :], rois)219            .squeeze(1)220        )221        output = output >= 0.5222        return output223 224    def get_bounding_boxes(self) -> Boxes:225        """226        Returns:227            Boxes: tight bounding boxes around bitmasks.228            If a mask is empty, it's bounding box will be all zero.229        """230        boxes = torch.zeros(self.tensor.shape[0], 4, dtype=torch.float32)231        x_any = torch.any(self.tensor, dim=1)232        y_any = torch.any(self.tensor, dim=2)233        for idx in range(self.tensor.shape[0]):234            x = torch.where(x_any[idx, :])[0]235            y = torch.where(y_any[idx, :])[0]236            if len(x) > 0 and len(y) > 0:237                boxes[idx, :] = torch.as_tensor(238                    [x[0], y[0], x[-1] + 1, y[-1] + 1], dtype=torch.float32239                )240        return Boxes(boxes)241 242    @staticmethod243    def cat(bitmasks_list: List["BitMasks"]) -> "BitMasks":244        """245        Concatenates a list of BitMasks into a single BitMasks246 247        Arguments:248            bitmasks_list (list[BitMasks])249 250        Returns:251            BitMasks: the concatenated BitMasks252        """253        assert isinstance(bitmasks_list, (list, tuple))254        assert len(bitmasks_list) > 0255        assert all(isinstance(bitmask, BitMasks) for bitmask in bitmasks_list)256 257        cat_bitmasks = type(bitmasks_list[0])(torch.cat([bm.tensor for bm in bitmasks_list], dim=0))258        return cat_bitmasks259 260 261class PolygonMasks:262    """263    This class stores the segmentation masks for all objects in one image, in the form of polygons.264 265    Attributes:266        polygons: list[list[ndarray]]. Each ndarray is a float64 vector representing a polygon.267    """268 269    def __init__(self, polygons: List[List[Union[torch.Tensor, np.ndarray]]]):270        """271        Arguments:272            polygons (list[list[np.ndarray]]): The first273                level of the list correspond to individual instances,274                the second level to all the polygons that compose the275                instance, and the third level to the polygon coordinates.276                The third level array should have the format of277                [x0, y0, x1, y1, ..., xn, yn] (n >= 3).278        """279        if not isinstance(polygons, list):280            raise ValueError(281                "Cannot create PolygonMasks: Expect a list of list of polygons per image. "282                "Got '{}' instead.".format(type(polygons))283            )284 285        def _make_array(t: Union[torch.Tensor, np.ndarray]) -> np.ndarray:286            # Use float64 for higher precision, because why not?287            # Always put polygons on CPU (self.to is a no-op) since they288            # are supposed to be small tensors.289            # May need to change this assumption if GPU placement becomes useful290            if isinstance(t, torch.Tensor):291                t = t.cpu().numpy()292            return np.asarray(t).astype("float64")293 294        def process_polygons(295            polygons_per_instance: List[Union[torch.Tensor, np.ndarray]]296        ) -> List[np.ndarray]:297            if not isinstance(polygons_per_instance, list):298                raise ValueError(299                    "Cannot create polygons: Expect a list of polygons per instance. "300                    "Got '{}' instead.".format(type(polygons_per_instance))301                )302            # transform each polygon to a numpy array303            polygons_per_instance = [_make_array(p) for p in polygons_per_instance]304            for polygon in polygons_per_instance:305                if len(polygon) % 2 != 0 or len(polygon) < 6:306                    raise ValueError(f"Cannot create a polygon from {len(polygon)} coordinates.")307            return polygons_per_instance308 309        self.polygons: List[List[np.ndarray]] = [310            process_polygons(polygons_per_instance) for polygons_per_instance in polygons311        ]312 313    def to(self, *args: Any, **kwargs: Any) -> "PolygonMasks":314        return self315 316    @property317    def device(self) -> torch.device:318        return torch.device("cpu")319 320    def get_bounding_boxes(self) -> Boxes:321        """322        Returns:323            Boxes: tight bounding boxes around polygon masks.324        """325        boxes = torch.zeros(len(self.polygons), 4, dtype=torch.float32)326        for idx, polygons_per_instance in enumerate(self.polygons):327            minxy = torch.as_tensor([float("inf"), float("inf")], dtype=torch.float32)328            maxxy = torch.zeros(2, dtype=torch.float32)329            for polygon in polygons_per_instance:330                coords = torch.from_numpy(polygon).view(-1, 2).to(dtype=torch.float32)331                minxy = torch.min(minxy, torch.min(coords, dim=0).values)332                maxxy = torch.max(maxxy, torch.max(coords, dim=0).values)333            boxes[idx, :2] = minxy334            boxes[idx, 2:] = maxxy335        return Boxes(boxes)336 337    def nonempty(self) -> torch.Tensor:338        """339        Find masks that are non-empty.340 341        Returns:342            Tensor:343                a BoolTensor which represents whether each mask is empty (False) or not (True).344        """345        keep = [1 if len(polygon) > 0 else 0 for polygon in self.polygons]346        return torch.from_numpy(np.asarray(keep, dtype=bool))347 348    def __getitem__(self, item: Union[int, slice, List[int], torch.BoolTensor]) -> "PolygonMasks":349        """350        Support indexing over the instances and return a `PolygonMasks` object.351        `item` can be:352 353        1. An integer. It will return an object with only one instance.354        2. A slice. It will return an object with the selected instances.355        3. A list[int]. It will return an object with the selected instances,356           correpsonding to the indices in the list.357        4. A vector mask of type BoolTensor, whose length is num_instances.358           It will return an object with the instances whose mask is nonzero.359        """360        if isinstance(item, int):361            selected_polygons = [self.polygons[item]]362        elif isinstance(item, slice):363            selected_polygons = self.polygons[item]364        elif isinstance(item, list):365            selected_polygons = [self.polygons[i] for i in item]366        elif isinstance(item, torch.Tensor):367            # Polygons is a list, so we have to move the indices back to CPU.368            if item.dtype == torch.bool:369                assert item.dim() == 1, item.shape370                item = item.nonzero().squeeze(1).cpu().numpy().tolist()371            elif item.dtype in [torch.int32, torch.int64]:372                item = item.cpu().numpy().tolist()373            else:374                raise ValueError("Unsupported tensor dtype={} for indexing!".format(item.dtype))375            selected_polygons = [self.polygons[i] for i in item]376        return PolygonMasks(selected_polygons)377 378    def __iter__(self) -> Iterator[List[np.ndarray]]:379        """380        Yields:381            list[ndarray]: the polygons for one instance.382            Each Tensor is a float64 vector representing a polygon.383        """384        return iter(self.polygons)385 386    def __repr__(self) -> str:387        s = self.__class__.__name__ + "("388        s += "num_instances={})".format(len(self.polygons))389        return s390 391    def __len__(self) -> int:392        return len(self.polygons)393 394    def crop_and_resize(self, boxes: torch.Tensor, mask_size: int) -> torch.Tensor:395        """396        Crop each mask by the given box, and resize results to (mask_size, mask_size).397        This can be used to prepare training targets for Mask R-CNN.398 399        Args:400            boxes (Tensor): Nx4 tensor storing the boxes for each mask401            mask_size (int): the size of the rasterized mask.402 403        Returns:404            Tensor: A bool tensor of shape (N, mask_size, mask_size), where405            N is the number of predicted boxes for this image.406        """407        assert len(boxes) == len(self), "{} != {}".format(len(boxes), len(self))408 409        device = boxes.device410        # Put boxes on the CPU, as the polygon representation is not efficient GPU-wise411        # (several small tensors for representing a single instance mask)412        boxes = boxes.to(torch.device("cpu"))413 414        results = [415            rasterize_polygons_within_box(poly, box.numpy(), mask_size)416            for poly, box in zip(self.polygons, boxes)417        ]418        """419        poly: list[list[float]], the polygons for one instance420        box: a tensor of shape (4,)421        """422        if len(results) == 0:423            return torch.empty(0, mask_size, mask_size, dtype=torch.bool, device=device)424        return torch.stack(results, dim=0).to(device=device)425 426    def area(self):427        """428        Computes area of the mask.429        Only works with Polygons, using the shoelace formula:430        https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates431 432        Returns:433            Tensor: a vector, area for each instance434        """435 436        area = []437        for polygons_per_instance in self.polygons:438            area_per_instance = 0439            for p in polygons_per_instance:440                area_per_instance += polygon_area(p[0::2], p[1::2])441            area.append(area_per_instance)442 443        return torch.tensor(area)444 445    @staticmethod446    def cat(polymasks_list: List["PolygonMasks"]) -> "PolygonMasks":447        """448        Concatenates a list of PolygonMasks into a single PolygonMasks449 450        Arguments:451            polymasks_list (list[PolygonMasks])452 453        Returns:454            PolygonMasks: the concatenated PolygonMasks455        """456        assert isinstance(polymasks_list, (list, tuple))457        assert len(polymasks_list) > 0458        assert all(isinstance(polymask, PolygonMasks) for polymask in polymasks_list)459 460        cat_polymasks = type(polymasks_list[0])(461            list(itertools.chain.from_iterable(pm.polygons for pm in polymasks_list))462        )463        return cat_polymasks464 465 466class ROIMasks:467    """468    Represent masks by N smaller masks defined in some ROIs. Once ROI boxes are given,469    full-image bitmask can be obtained by "pasting" the mask on the region defined470    by the corresponding ROI box.471    """472 473    def __init__(self, tensor: torch.Tensor):474        """475        Args:476            tensor: (N, M, M) mask tensor that defines the mask within each ROI.477        """478        if tensor.dim() != 3:479            raise ValueError("ROIMasks must take a masks of 3 dimension.")480        self.tensor = tensor481 482    def to(self, device: torch.device) -> "ROIMasks":483        return ROIMasks(self.tensor.to(device))484 485    @property486    def device(self) -> device:487        return self.tensor.device488 489    def __len__(self):490        return self.tensor.shape[0]491 492    def __getitem__(self, item) -> "ROIMasks":493        """494        Returns:495            ROIMasks: Create a new :class:`ROIMasks` by indexing.496 497        The following usage are allowed:498 499        1. `new_masks = masks[2:10]`: return a slice of masks.500        2. `new_masks = masks[vector]`, where vector is a torch.BoolTensor501           with `length = len(masks)`. Nonzero elements in the vector will be selected.502 503        Note that the returned object might share storage with this object,504        subject to Pytorch's indexing semantics.505        """506        t = self.tensor[item]507        if t.dim() != 3:508            raise ValueError(509                f"Indexing on ROIMasks with {item} returns a tensor with shape {t.shape}!"510            )511        return ROIMasks(t)512 513    @torch.jit.unused514    def __repr__(self) -> str:515        s = self.__class__.__name__ + "("516        s += "num_instances={})".format(len(self.tensor))517        return s518 519    @torch.jit.unused520    def to_bitmasks(self, boxes: torch.Tensor, height, width, threshold=0.5):521        """522        Args: see documentation of :func:`paste_masks_in_image`.523        """524        from detectron2.layers.mask_ops import paste_masks_in_image, _paste_masks_tensor_shape525 526        if torch.jit.is_tracing():527            if isinstance(height, torch.Tensor):528                paste_func = _paste_masks_tensor_shape529            else:530                paste_func = paste_masks_in_image531        else:532            paste_func = retry_if_cuda_oom(paste_masks_in_image)533        bitmasks = paste_func(self.tensor, boxes.tensor, (height, width), threshold=threshold)534        return BitMasks(bitmasks)535