Team Ai
Apppublic

Arulkumar03/Wheat_HEAD_Detection_Counting_ComputerVision_Model

sourceHugging Facemitupdated 3y agoView on Hugging Face
0likes
augmentation_impl.py737 linesDownload Raw Back to transforms
1# -*- coding: utf-8 -*-2# Copyright (c) Facebook, Inc. and its affiliates.3"""4Implement many useful :class:`Augmentation`.5"""6import numpy as np7import sys8from numpy import random9from typing import Tuple10import torch11from fvcore.transforms.transform import (12    BlendTransform,13    CropTransform,14    HFlipTransform,15    NoOpTransform,16    PadTransform,17    Transform,18    TransformList,19    VFlipTransform,20)21from PIL import Image22 23from detectron2.structures import Boxes, pairwise_iou24 25from .augmentation import Augmentation, _transform_to_aug26from .transform import ExtentTransform, ResizeTransform, RotationTransform27 28__all__ = [29    "FixedSizeCrop",30    "RandomApply",31    "RandomBrightness",32    "RandomContrast",33    "RandomCrop",34    "RandomExtent",35    "RandomFlip",36    "RandomSaturation",37    "RandomLighting",38    "RandomRotation",39    "Resize",40    "ResizeScale",41    "ResizeShortestEdge",42    "RandomCrop_CategoryAreaConstraint",43    "RandomResize",44    "MinIoURandomCrop",45]46 47 48class RandomApply(Augmentation):49    """50    Randomly apply an augmentation with a given probability.51    """52 53    def __init__(self, tfm_or_aug, prob=0.5):54        """55        Args:56            tfm_or_aug (Transform, Augmentation): the transform or augmentation57                to be applied. It can either be a `Transform` or `Augmentation`58                instance.59            prob (float): probability between 0.0 and 1.0 that60                the wrapper transformation is applied61        """62        super().__init__()63        self.aug = _transform_to_aug(tfm_or_aug)64        assert 0.0 <= prob <= 1.0, f"Probablity must be between 0.0 and 1.0 (given: {prob})"65        self.prob = prob66 67    def get_transform(self, *args):68        do = self._rand_range() < self.prob69        if do:70            return self.aug.get_transform(*args)71        else:72            return NoOpTransform()73 74    def __call__(self, aug_input):75        do = self._rand_range() < self.prob76        if do:77            return self.aug(aug_input)78        else:79            return NoOpTransform()80 81 82class RandomFlip(Augmentation):83    """84    Flip the image horizontally or vertically with the given probability.85    """86 87    def __init__(self, prob=0.5, *, horizontal=True, vertical=False):88        """89        Args:90            prob (float): probability of flip.91            horizontal (boolean): whether to apply horizontal flipping92            vertical (boolean): whether to apply vertical flipping93        """94        super().__init__()95 96        if horizontal and vertical:97            raise ValueError("Cannot do both horiz and vert. Please use two Flip instead.")98        if not horizontal and not vertical:99            raise ValueError("At least one of horiz or vert has to be True!")100        self._init(locals())101 102    def get_transform(self, image):103        h, w = image.shape[:2]104        do = self._rand_range() < self.prob105        if do:106            if self.horizontal:107                return HFlipTransform(w)108            elif self.vertical:109                return VFlipTransform(h)110        else:111            return NoOpTransform()112 113 114class Resize(Augmentation):115    """Resize image to a fixed target size"""116 117    def __init__(self, shape, interp=Image.BILINEAR):118        """119        Args:120            shape: (h, w) tuple or a int121            interp: PIL interpolation method122        """123        if isinstance(shape, int):124            shape = (shape, shape)125        shape = tuple(shape)126        self._init(locals())127 128    def get_transform(self, image):129        return ResizeTransform(130            image.shape[0], image.shape[1], self.shape[0], self.shape[1], self.interp131        )132 133 134class ResizeShortestEdge(Augmentation):135    """136    Resize the image while keeping the aspect ratio unchanged.137    It attempts to scale the shorter edge to the given `short_edge_length`,138    as long as the longer edge does not exceed `max_size`.139    If `max_size` is reached, then downscale so that the longer edge does not exceed max_size.140    """141 142    @torch.jit.unused143    def __init__(144        self, short_edge_length, max_size=sys.maxsize, sample_style="range", interp=Image.BILINEAR145    ):146        """147        Args:148            short_edge_length (list[int]): If ``sample_style=="range"``,149                a [min, max] interval from which to sample the shortest edge length.150                If ``sample_style=="choice"``, a list of shortest edge lengths to sample from.151            max_size (int): maximum allowed longest edge length.152            sample_style (str): either "range" or "choice".153        """154        super().__init__()155        assert sample_style in ["range", "choice"], sample_style156 157        self.is_range = sample_style == "range"158        if isinstance(short_edge_length, int):159            short_edge_length = (short_edge_length, short_edge_length)160        if self.is_range:161            assert len(short_edge_length) == 2, (162                "short_edge_length must be two values using 'range' sample style."163                f" Got {short_edge_length}!"164            )165        self._init(locals())166 167    @torch.jit.unused168    def get_transform(self, image):169        h, w = image.shape[:2]170        if self.is_range:171            size = np.random.randint(self.short_edge_length[0], self.short_edge_length[1] + 1)172        else:173            size = np.random.choice(self.short_edge_length)174        if size == 0:175            return NoOpTransform()176 177        newh, neww = ResizeShortestEdge.get_output_shape(h, w, size, self.max_size)178        return ResizeTransform(h, w, newh, neww, self.interp)179 180    @staticmethod181    def get_output_shape(182        oldh: int, oldw: int, short_edge_length: int, max_size: int183    ) -> Tuple[int, int]:184        """185        Compute the output size given input size and target short edge length.186        """187        h, w = oldh, oldw188        size = short_edge_length * 1.0189        scale = size / min(h, w)190        if h < w:191            newh, neww = size, scale * w192        else:193            newh, neww = scale * h, size194        if max(newh, neww) > max_size:195            scale = max_size * 1.0 / max(newh, neww)196            newh = newh * scale197            neww = neww * scale198        neww = int(neww + 0.5)199        newh = int(newh + 0.5)200        return (newh, neww)201 202 203class ResizeScale(Augmentation):204    """205    Takes target size as input and randomly scales the given target size between `min_scale`206    and `max_scale`. It then scales the input image such that it fits inside the scaled target207    box, keeping the aspect ratio constant.208    This implements the resize part of the Google's 'resize_and_crop' data augmentation:209    https://github.com/tensorflow/tpu/blob/master/models/official/detection/utils/input_utils.py#L127210    """211 212    def __init__(213        self,214        min_scale: float,215        max_scale: float,216        target_height: int,217        target_width: int,218        interp: int = Image.BILINEAR,219    ):220        """221        Args:222            min_scale: minimum image scale range.223            max_scale: maximum image scale range.224            target_height: target image height.225            target_width: target image width.226            interp: image interpolation method.227        """228        super().__init__()229        self._init(locals())230 231    def _get_resize(self, image: np.ndarray, scale: float) -> Transform:232        input_size = image.shape[:2]233 234        # Compute new target size given a scale.235        target_size = (self.target_height, self.target_width)236        target_scale_size = np.multiply(target_size, scale)237 238        # Compute actual rescaling applied to input image and output size.239        output_scale = np.minimum(240            target_scale_size[0] / input_size[0], target_scale_size[1] / input_size[1]241        )242        output_size = np.round(np.multiply(input_size, output_scale)).astype(int)243 244        return ResizeTransform(245            input_size[0], input_size[1], output_size[0], output_size[1], self.interp246        )247 248    def get_transform(self, image: np.ndarray) -> Transform:249        random_scale = np.random.uniform(self.min_scale, self.max_scale)250        return self._get_resize(image, random_scale)251 252 253class RandomRotation(Augmentation):254    """255    This method returns a copy of this image, rotated the given256    number of degrees counter clockwise around the given center.257    """258 259    def __init__(self, angle, expand=True, center=None, sample_style="range", interp=None):260        """261        Args:262            angle (list[float]): If ``sample_style=="range"``,263                a [min, max] interval from which to sample the angle (in degrees).264                If ``sample_style=="choice"``, a list of angles to sample from265            expand (bool): choose if the image should be resized to fit the whole266                rotated image (default), or simply cropped267            center (list[[float, float]]):  If ``sample_style=="range"``,268                a [[minx, miny], [maxx, maxy]] relative interval from which to sample the center,269                [0, 0] being the top left of the image and [1, 1] the bottom right.270                If ``sample_style=="choice"``, a list of centers to sample from271                Default: None, which means that the center of rotation is the center of the image272                center has no effect if expand=True because it only affects shifting273        """274        super().__init__()275        assert sample_style in ["range", "choice"], sample_style276        self.is_range = sample_style == "range"277        if isinstance(angle, (float, int)):278            angle = (angle, angle)279        if center is not None and isinstance(center[0], (float, int)):280            center = (center, center)281        self._init(locals())282 283    def get_transform(self, image):284        h, w = image.shape[:2]285        center = None286        if self.is_range:287            angle = np.random.uniform(self.angle[0], self.angle[1])288            if self.center is not None:289                center = (290                    np.random.uniform(self.center[0][0], self.center[1][0]),291                    np.random.uniform(self.center[0][1], self.center[1][1]),292                )293        else:294            angle = np.random.choice(self.angle)295            if self.center is not None:296                center = np.random.choice(self.center)297 298        if center is not None:299            center = (w * center[0], h * center[1])  # Convert to absolute coordinates300 301        if angle % 360 == 0:302            return NoOpTransform()303 304        return RotationTransform(h, w, angle, expand=self.expand, center=center, interp=self.interp)305 306 307class FixedSizeCrop(Augmentation):308    """309    If `crop_size` is smaller than the input image size, then it uses a random crop of310    the crop size. If `crop_size` is larger than the input image size, then it pads311    the right and the bottom of the image to the crop size if `pad` is True, otherwise312    it returns the smaller image.313    """314 315    def __init__(316        self,317        crop_size: Tuple[int],318        pad: bool = True,319        pad_value: float = 128.0,320        seg_pad_value: int = 255,321    ):322        """323        Args:324            crop_size: target image (height, width).325            pad: if True, will pad images smaller than `crop_size` up to `crop_size`326            pad_value: the padding value to the image.327            seg_pad_value: the padding value to the segmentation mask.328        """329        super().__init__()330        self._init(locals())331 332    def _get_crop(self, image: np.ndarray) -> Transform:333        # Compute the image scale and scaled size.334        input_size = image.shape[:2]335        output_size = self.crop_size336 337        # Add random crop if the image is scaled up.338        max_offset = np.subtract(input_size, output_size)339        max_offset = np.maximum(max_offset, 0)340        offset = np.multiply(max_offset, np.random.uniform(0.0, 1.0))341        offset = np.round(offset).astype(int)342        return CropTransform(343            offset[1], offset[0], output_size[1], output_size[0], input_size[1], input_size[0]344        )345 346    def _get_pad(self, image: np.ndarray) -> Transform:347        # Compute the image scale and scaled size.348        input_size = image.shape[:2]349        output_size = self.crop_size350 351        # Add padding if the image is scaled down.352        pad_size = np.subtract(output_size, input_size)353        pad_size = np.maximum(pad_size, 0)354        original_size = np.minimum(input_size, output_size)355        return PadTransform(356            0,357            0,358            pad_size[1],359            pad_size[0],360            original_size[1],361            original_size[0],362            self.pad_value,363            self.seg_pad_value,364        )365 366    def get_transform(self, image: np.ndarray) -> TransformList:367        transforms = [self._get_crop(image)]368        if self.pad:369            transforms.append(self._get_pad(image))370        return TransformList(transforms)371 372 373class RandomCrop(Augmentation):374    """375    Randomly crop a rectangle region out of an image.376    """377 378    def __init__(self, crop_type: str, crop_size):379        """380        Args:381            crop_type (str): one of "relative_range", "relative", "absolute", "absolute_range".382            crop_size (tuple[float, float]): two floats, explained below.383 384        - "relative": crop a (H * crop_size[0], W * crop_size[1]) region from an input image of385          size (H, W). crop size should be in (0, 1]386        - "relative_range": uniformly sample two values from [crop_size[0], 1]387          and [crop_size[1]], 1], and use them as in "relative" crop type.388        - "absolute" crop a (crop_size[0], crop_size[1]) region from input image.389          crop_size must be smaller than the input image size.390        - "absolute_range", for an input of size (H, W), uniformly sample H_crop in391          [crop_size[0], min(H, crop_size[1])] and W_crop in [crop_size[0], min(W, crop_size[1])].392          Then crop a region (H_crop, W_crop).393        """394        # TODO style of relative_range and absolute_range are not consistent:395        # one takes (h, w) but another takes (min, max)396        super().__init__()397        assert crop_type in ["relative_range", "relative", "absolute", "absolute_range"]398        self._init(locals())399 400    def get_transform(self, image):401        h, w = image.shape[:2]402        croph, cropw = self.get_crop_size((h, w))403        assert h >= croph and w >= cropw, "Shape computation in {} has bugs.".format(self)404        h0 = np.random.randint(h - croph + 1)405        w0 = np.random.randint(w - cropw + 1)406        return CropTransform(w0, h0, cropw, croph)407 408    def get_crop_size(self, image_size):409        """410        Args:411            image_size (tuple): height, width412 413        Returns:414            crop_size (tuple): height, width in absolute pixels415        """416        h, w = image_size417        if self.crop_type == "relative":418            ch, cw = self.crop_size419            return int(h * ch + 0.5), int(w * cw + 0.5)420        elif self.crop_type == "relative_range":421            crop_size = np.asarray(self.crop_size, dtype=np.float32)422            ch, cw = crop_size + np.random.rand(2) * (1 - crop_size)423            return int(h * ch + 0.5), int(w * cw + 0.5)424        elif self.crop_type == "absolute":425            return (min(self.crop_size[0], h), min(self.crop_size[1], w))426        elif self.crop_type == "absolute_range":427            assert self.crop_size[0] <= self.crop_size[1]428            ch = np.random.randint(min(h, self.crop_size[0]), min(h, self.crop_size[1]) + 1)429            cw = np.random.randint(min(w, self.crop_size[0]), min(w, self.crop_size[1]) + 1)430            return ch, cw431        else:432            raise NotImplementedError("Unknown crop type {}".format(self.crop_type))433 434 435class RandomCrop_CategoryAreaConstraint(Augmentation):436    """437    Similar to :class:`RandomCrop`, but find a cropping window such that no single category438    occupies a ratio of more than `single_category_max_area` in semantic segmentation ground439    truth, which can cause unstability in training. The function attempts to find such a valid440    cropping window for at most 10 times.441    """442 443    def __init__(444        self,445        crop_type: str,446        crop_size,447        single_category_max_area: float = 1.0,448        ignored_category: int = None,449    ):450        """451        Args:452            crop_type, crop_size: same as in :class:`RandomCrop`453            single_category_max_area: the maximum allowed area ratio of a454                category. Set to 1.0 to disable455            ignored_category: allow this category in the semantic segmentation456                ground truth to exceed the area ratio. Usually set to the category457                that's ignored in training.458        """459        self.crop_aug = RandomCrop(crop_type, crop_size)460        self._init(locals())461 462    def get_transform(self, image, sem_seg):463        if self.single_category_max_area >= 1.0:464            return self.crop_aug.get_transform(image)465        else:466            h, w = sem_seg.shape467            for _ in range(10):468                crop_size = self.crop_aug.get_crop_size((h, w))469                y0 = np.random.randint(h - crop_size[0] + 1)470                x0 = np.random.randint(w - crop_size[1] + 1)471                sem_seg_temp = sem_seg[y0 : y0 + crop_size[0], x0 : x0 + crop_size[1]]472                labels, cnt = np.unique(sem_seg_temp, return_counts=True)473                if self.ignored_category is not None:474                    cnt = cnt[labels != self.ignored_category]475                if len(cnt) > 1 and np.max(cnt) < np.sum(cnt) * self.single_category_max_area:476                    break477            crop_tfm = CropTransform(x0, y0, crop_size[1], crop_size[0])478            return crop_tfm479 480 481class RandomExtent(Augmentation):482    """483    Outputs an image by cropping a random "subrect" of the source image.484 485    The subrect can be parameterized to include pixels outside the source image,486    in which case they will be set to zeros (i.e. black). The size of the output487    image will vary with the size of the random subrect.488    """489 490    def __init__(self, scale_range, shift_range):491        """492        Args:493            output_size (h, w): Dimensions of output image494            scale_range (l, h): Range of input-to-output size scaling factor495            shift_range (x, y): Range of shifts of the cropped subrect. The rect496                is shifted by [w / 2 * Uniform(-x, x), h / 2 * Uniform(-y, y)],497                where (w, h) is the (width, height) of the input image. Set each498                component to zero to crop at the image's center.499        """500        super().__init__()501        self._init(locals())502 503    def get_transform(self, image):504        img_h, img_w = image.shape[:2]505 506        # Initialize src_rect to fit the input image.507        src_rect = np.array([-0.5 * img_w, -0.5 * img_h, 0.5 * img_w, 0.5 * img_h])508 509        # Apply a random scaling to the src_rect.510        src_rect *= np.random.uniform(self.scale_range[0], self.scale_range[1])511 512        # Apply a random shift to the coordinates origin.513        src_rect[0::2] += self.shift_range[0] * img_w * (np.random.rand() - 0.5)514        src_rect[1::2] += self.shift_range[1] * img_h * (np.random.rand() - 0.5)515 516        # Map src_rect coordinates into image coordinates (center at corner).517        src_rect[0::2] += 0.5 * img_w518        src_rect[1::2] += 0.5 * img_h519 520        return ExtentTransform(521            src_rect=(src_rect[0], src_rect[1], src_rect[2], src_rect[3]),522            output_size=(int(src_rect[3] - src_rect[1]), int(src_rect[2] - src_rect[0])),523        )524 525 526class RandomContrast(Augmentation):527    """528    Randomly transforms image contrast.529 530    Contrast intensity is uniformly sampled in (intensity_min, intensity_max).531    - intensity < 1 will reduce contrast532    - intensity = 1 will preserve the input image533    - intensity > 1 will increase contrast534 535    See: https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html536    """537 538    def __init__(self, intensity_min, intensity_max):539        """540        Args:541            intensity_min (float): Minimum augmentation542            intensity_max (float): Maximum augmentation543        """544        super().__init__()545        self._init(locals())546 547    def get_transform(self, image):548        w = np.random.uniform(self.intensity_min, self.intensity_max)549        return BlendTransform(src_image=image.mean(), src_weight=1 - w, dst_weight=w)550 551 552class RandomBrightness(Augmentation):553    """554    Randomly transforms image brightness.555 556    Brightness intensity is uniformly sampled in (intensity_min, intensity_max).557    - intensity < 1 will reduce brightness558    - intensity = 1 will preserve the input image559    - intensity > 1 will increase brightness560 561    See: https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html562    """563 564    def __init__(self, intensity_min, intensity_max):565        """566        Args:567            intensity_min (float): Minimum augmentation568            intensity_max (float): Maximum augmentation569        """570        super().__init__()571        self._init(locals())572 573    def get_transform(self, image):574        w = np.random.uniform(self.intensity_min, self.intensity_max)575        return BlendTransform(src_image=0, src_weight=1 - w, dst_weight=w)576 577 578class RandomSaturation(Augmentation):579    """580    Randomly transforms saturation of an RGB image.581    Input images are assumed to have 'RGB' channel order.582 583    Saturation intensity is uniformly sampled in (intensity_min, intensity_max).584    - intensity < 1 will reduce saturation (make the image more grayscale)585    - intensity = 1 will preserve the input image586    - intensity > 1 will increase saturation587 588    See: https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html589    """590 591    def __init__(self, intensity_min, intensity_max):592        """593        Args:594            intensity_min (float): Minimum augmentation (1 preserves input).595            intensity_max (float): Maximum augmentation (1 preserves input).596        """597        super().__init__()598        self._init(locals())599 600    def get_transform(self, image):601        assert image.shape[-1] == 3, "RandomSaturation only works on RGB images"602        w = np.random.uniform(self.intensity_min, self.intensity_max)603        grayscale = image.dot([0.299, 0.587, 0.114])[:, :, np.newaxis]604        return BlendTransform(src_image=grayscale, src_weight=1 - w, dst_weight=w)605 606 607class RandomLighting(Augmentation):608    """609    The "lighting" augmentation described in AlexNet, using fixed PCA over ImageNet.610    Input images are assumed to have 'RGB' channel order.611 612    The degree of color jittering is randomly sampled via a normal distribution,613    with standard deviation given by the scale parameter.614    """615 616    def __init__(self, scale):617        """618        Args:619            scale (float): Standard deviation of principal component weighting.620        """621        super().__init__()622        self._init(locals())623        self.eigen_vecs = np.array(624            [[-0.5675, 0.7192, 0.4009], [-0.5808, -0.0045, -0.8140], [-0.5836, -0.6948, 0.4203]]625        )626        self.eigen_vals = np.array([0.2175, 0.0188, 0.0045])627 628    def get_transform(self, image):629        assert image.shape[-1] == 3, "RandomLighting only works on RGB images"630        weights = np.random.normal(scale=self.scale, size=3)631        return BlendTransform(632            src_image=self.eigen_vecs.dot(weights * self.eigen_vals), src_weight=1.0, dst_weight=1.0633        )634 635 636class RandomResize(Augmentation):637    """Randomly resize image to a target size in shape_list"""638 639    def __init__(self, shape_list, interp=Image.BILINEAR):640        """641        Args:642            shape_list: a list of shapes in (h, w)643            interp: PIL interpolation method644        """645        self.shape_list = shape_list646        self._init(locals())647 648    def get_transform(self, image):649        shape_idx = np.random.randint(low=0, high=len(self.shape_list))650        h, w = self.shape_list[shape_idx]651        return ResizeTransform(image.shape[0], image.shape[1], h, w, self.interp)652 653 654class MinIoURandomCrop(Augmentation):655    """Random crop the image & bboxes, the cropped patches have minimum IoU656    requirement with original image & bboxes, the IoU threshold is randomly657    selected from min_ious.658 659    Args:660        min_ious (tuple): minimum IoU threshold for all intersections with661        bounding boxes662        min_crop_size (float): minimum crop's size (i.e. h,w := a*h, a*w,663        where a >= min_crop_size)664        mode_trials: number of trials for sampling min_ious threshold665        crop_trials: number of trials for sampling crop_size after cropping666    """667 668    def __init__(669        self,670        min_ious=(0.1, 0.3, 0.5, 0.7, 0.9),671        min_crop_size=0.3,672        mode_trials=1000,673        crop_trials=50,674    ):675        self.min_ious = min_ious676        self.sample_mode = (1, *min_ious, 0)677        self.min_crop_size = min_crop_size678        self.mode_trials = mode_trials679        self.crop_trials = crop_trials680 681    def get_transform(self, image, boxes):682        """Call function to crop images and bounding boxes with minimum IoU683        constraint.684 685        Args:686            boxes: ground truth boxes in (x1, y1, x2, y2) format687        """688        if boxes is None:689            return NoOpTransform()690        h, w, c = image.shape691        for _ in range(self.mode_trials):692            mode = random.choice(self.sample_mode)693            self.mode = mode694            if mode == 1:695                return NoOpTransform()696 697            min_iou = mode698            for _ in range(self.crop_trials):699                new_w = random.uniform(self.min_crop_size * w, w)700                new_h = random.uniform(self.min_crop_size * h, h)701 702                # h / w in [0.5, 2]703                if new_h / new_w < 0.5 or new_h / new_w > 2:704                    continue705 706                left = random.uniform(w - new_w)707                top = random.uniform(h - new_h)708 709                patch = np.array((int(left), int(top), int(left + new_w), int(top + new_h)))710                # Line or point crop is not allowed711                if patch[2] == patch[0] or patch[3] == patch[1]:712                    continue713                overlaps = pairwise_iou(714                    Boxes(patch.reshape(-1, 4)), Boxes(boxes.reshape(-1, 4))715                ).reshape(-1)716                if len(overlaps) > 0 and overlaps.min() < min_iou:717                    continue718 719                # center of boxes should inside the crop img720                # only adjust boxes and instance masks when the gt is not empty721                if len(overlaps) > 0:722                    # adjust boxes723                    def is_center_of_bboxes_in_patch(boxes, patch):724                        center = (boxes[:, :2] + boxes[:, 2:]) / 2725                        mask = (726                            (center[:, 0] > patch[0])727                            * (center[:, 1] > patch[1])728                            * (center[:, 0] < patch[2])729                            * (center[:, 1] < patch[3])730                        )731                        return mask732 733                    mask = is_center_of_bboxes_in_patch(boxes, patch)734                    if not mask.any():735                        continue736                return CropTransform(int(left), int(top), int(new_w), int(new_h))737