CoolFace
Apppublic

coreml-community/ControlNet-v1-1-Annotators-cpu

sourceHugging Facemitupdated 2y agoView on Hugging Face
15likes
detection_utils.py660 linesDownload Raw Back to data
1# -*- coding: utf-8 -*-2# Copyright (c) Facebook, Inc. and its affiliates.3 4"""5Common data processing utilities that are used in a6typical object detection data pipeline.7"""8import logging9import numpy as np10from typing import List, Union11import pycocotools.mask as mask_util12import torch13from PIL import Image14 15from annotator.oneformer.detectron2.structures import (16    BitMasks,17    Boxes,18    BoxMode,19    Instances,20    Keypoints,21    PolygonMasks,22    RotatedBoxes,23    polygons_to_bitmask,24)25from annotator.oneformer.detectron2.utils.file_io import PathManager26 27from . import transforms as T28from .catalog import MetadataCatalog29 30__all__ = [31    "SizeMismatchError",32    "convert_image_to_rgb",33    "check_image_size",34    "transform_proposals",35    "transform_instance_annotations",36    "annotations_to_instances",37    "annotations_to_instances_rotated",38    "build_augmentation",39    "build_transform_gen",40    "create_keypoint_hflip_indices",41    "filter_empty_instances",42    "read_image",43]44 45 46class SizeMismatchError(ValueError):47    """48    When loaded image has difference width/height compared with annotation.49    """50 51 52# https://en.wikipedia.org/wiki/YUV#SDTV_with_BT.60153_M_RGB2YUV = [[0.299, 0.587, 0.114], [-0.14713, -0.28886, 0.436], [0.615, -0.51499, -0.10001]]54_M_YUV2RGB = [[1.0, 0.0, 1.13983], [1.0, -0.39465, -0.58060], [1.0, 2.03211, 0.0]]55 56# https://www.exiv2.org/tags.html57_EXIF_ORIENT = 274  # exif 'Orientation' tag58 59 60def convert_PIL_to_numpy(image, format):61    """62    Convert PIL image to numpy array of target format.63 64    Args:65        image (PIL.Image): a PIL image66        format (str): the format of output image67 68    Returns:69        (np.ndarray): also see `read_image`70    """71    if format is not None:72        # PIL only supports RGB, so convert to RGB and flip channels over below73        conversion_format = format74        if format in ["BGR", "YUV-BT.601"]:75            conversion_format = "RGB"76        image = image.convert(conversion_format)77    image = np.asarray(image)78    # PIL squeezes out the channel dimension for "L", so make it HWC79    if format == "L":80        image = np.expand_dims(image, -1)81 82    # handle formats not supported by PIL83    elif format == "BGR":84        # flip channels if needed85        image = image[:, :, ::-1]86    elif format == "YUV-BT.601":87        image = image / 255.088        image = np.dot(image, np.array(_M_RGB2YUV).T)89 90    return image91 92 93def convert_image_to_rgb(image, format):94    """95    Convert an image from given format to RGB.96 97    Args:98        image (np.ndarray or Tensor): an HWC image99        format (str): the format of input image, also see `read_image`100 101    Returns:102        (np.ndarray): (H,W,3) RGB image in 0-255 range, can be either float or uint8103    """104    if isinstance(image, torch.Tensor):105        image = image.cpu().numpy()106    if format == "BGR":107        image = image[:, :, [2, 1, 0]]108    elif format == "YUV-BT.601":109        image = np.dot(image, np.array(_M_YUV2RGB).T)110        image = image * 255.0111    else:112        if format == "L":113            image = image[:, :, 0]114        image = image.astype(np.uint8)115        image = np.asarray(Image.fromarray(image, mode=format).convert("RGB"))116    return image117 118 119def _apply_exif_orientation(image):120    """121    Applies the exif orientation correctly.122 123    This code exists per the bug:124      https://github.com/python-pillow/Pillow/issues/3973125    with the function `ImageOps.exif_transpose`. The Pillow source raises errors with126    various methods, especially `tobytes`127 128    Function based on:129      https://github.com/wkentaro/labelme/blob/v4.5.4/labelme/utils/image.py#L59130      https://github.com/python-pillow/Pillow/blob/7.1.2/src/PIL/ImageOps.py#L527131 132    Args:133        image (PIL.Image): a PIL image134 135    Returns:136        (PIL.Image): the PIL image with exif orientation applied, if applicable137    """138    if not hasattr(image, "getexif"):139        return image140 141    try:142        exif = image.getexif()143    except Exception:  # https://github.com/facebookresearch/detectron2/issues/1885144        exif = None145 146    if exif is None:147        return image148 149    orientation = exif.get(_EXIF_ORIENT)150 151    method = {152        2: Image.FLIP_LEFT_RIGHT,153        3: Image.ROTATE_180,154        4: Image.FLIP_TOP_BOTTOM,155        5: Image.TRANSPOSE,156        6: Image.ROTATE_270,157        7: Image.TRANSVERSE,158        8: Image.ROTATE_90,159    }.get(orientation)160 161    if method is not None:162        return image.transpose(method)163    return image164 165 166def read_image(file_name, format=None):167    """168    Read an image into the given format.169    Will apply rotation and flipping if the image has such exif information.170 171    Args:172        file_name (str): image file path173        format (str): one of the supported image modes in PIL, or "BGR" or "YUV-BT.601".174 175    Returns:176        image (np.ndarray):177            an HWC image in the given format, which is 0-255, uint8 for178            supported image modes in PIL or "BGR"; float (0-1 for Y) for YUV-BT.601.179    """180    with PathManager.open(file_name, "rb") as f:181        image = Image.open(f)182 183        # work around this bug: https://github.com/python-pillow/Pillow/issues/3973184        image = _apply_exif_orientation(image)185        return convert_PIL_to_numpy(image, format)186 187 188def check_image_size(dataset_dict, image):189    """190    Raise an error if the image does not match the size specified in the dict.191    """192    if "width" in dataset_dict or "height" in dataset_dict:193        image_wh = (image.shape[1], image.shape[0])194        expected_wh = (dataset_dict["width"], dataset_dict["height"])195        if not image_wh == expected_wh:196            raise SizeMismatchError(197                "Mismatched image shape{}, got {}, expect {}.".format(198                    " for image " + dataset_dict["file_name"]199                    if "file_name" in dataset_dict200                    else "",201                    image_wh,202                    expected_wh,203                )204                + " Please check the width/height in your annotation."205            )206 207    # To ensure bbox always remap to original image size208    if "width" not in dataset_dict:209        dataset_dict["width"] = image.shape[1]210    if "height" not in dataset_dict:211        dataset_dict["height"] = image.shape[0]212 213 214def transform_proposals(dataset_dict, image_shape, transforms, *, proposal_topk, min_box_size=0):215    """216    Apply transformations to the proposals in dataset_dict, if any.217 218    Args:219        dataset_dict (dict): a dict read from the dataset, possibly220            contains fields "proposal_boxes", "proposal_objectness_logits", "proposal_bbox_mode"221        image_shape (tuple): height, width222        transforms (TransformList):223        proposal_topk (int): only keep top-K scoring proposals224        min_box_size (int): proposals with either side smaller than this225            threshold are removed226 227    The input dict is modified in-place, with abovementioned keys removed. A new228    key "proposals" will be added. Its value is an `Instances`229    object which contains the transformed proposals in its field230    "proposal_boxes" and "objectness_logits".231    """232    if "proposal_boxes" in dataset_dict:233        # Transform proposal boxes234        boxes = transforms.apply_box(235            BoxMode.convert(236                dataset_dict.pop("proposal_boxes"),237                dataset_dict.pop("proposal_bbox_mode"),238                BoxMode.XYXY_ABS,239            )240        )241        boxes = Boxes(boxes)242        objectness_logits = torch.as_tensor(243            dataset_dict.pop("proposal_objectness_logits").astype("float32")244        )245 246        boxes.clip(image_shape)247        keep = boxes.nonempty(threshold=min_box_size)248        boxes = boxes[keep]249        objectness_logits = objectness_logits[keep]250 251        proposals = Instances(image_shape)252        proposals.proposal_boxes = boxes[:proposal_topk]253        proposals.objectness_logits = objectness_logits[:proposal_topk]254        dataset_dict["proposals"] = proposals255 256 257def get_bbox(annotation):258    """259    Get bbox from data260    Args:261        annotation (dict): dict of instance annotations for a single instance.262    Returns:263        bbox (ndarray): x1, y1, x2, y2 coordinates264    """265    # bbox is 1d (per-instance bounding box)266    bbox = BoxMode.convert(annotation["bbox"], annotation["bbox_mode"], BoxMode.XYXY_ABS)267    return bbox268 269 270def transform_instance_annotations(271    annotation, transforms, image_size, *, keypoint_hflip_indices=None272):273    """274    Apply transforms to box, segmentation and keypoints annotations of a single instance.275 276    It will use `transforms.apply_box` for the box, and277    `transforms.apply_coords` for segmentation polygons & keypoints.278    If you need anything more specially designed for each data structure,279    you'll need to implement your own version of this function or the transforms.280 281    Args:282        annotation (dict): dict of instance annotations for a single instance.283            It will be modified in-place.284        transforms (TransformList or list[Transform]):285        image_size (tuple): the height, width of the transformed image286        keypoint_hflip_indices (ndarray[int]): see `create_keypoint_hflip_indices`.287 288    Returns:289        dict:290            the same input dict with fields "bbox", "segmentation", "keypoints"291            transformed according to `transforms`.292            The "bbox_mode" field will be set to XYXY_ABS.293    """294    if isinstance(transforms, (tuple, list)):295        transforms = T.TransformList(transforms)296    # bbox is 1d (per-instance bounding box)297    bbox = BoxMode.convert(annotation["bbox"], annotation["bbox_mode"], BoxMode.XYXY_ABS)298    # clip transformed bbox to image size299    bbox = transforms.apply_box(np.array([bbox]))[0].clip(min=0)300    annotation["bbox"] = np.minimum(bbox, list(image_size + image_size)[::-1])301    annotation["bbox_mode"] = BoxMode.XYXY_ABS302 303    if "segmentation" in annotation:304        # each instance contains 1 or more polygons305        segm = annotation["segmentation"]306        if isinstance(segm, list):307            # polygons308            polygons = [np.asarray(p).reshape(-1, 2) for p in segm]309            annotation["segmentation"] = [310                p.reshape(-1) for p in transforms.apply_polygons(polygons)311            ]312        elif isinstance(segm, dict):313            # RLE314            mask = mask_util.decode(segm)315            mask = transforms.apply_segmentation(mask)316            assert tuple(mask.shape[:2]) == image_size317            annotation["segmentation"] = mask318        else:319            raise ValueError(320                "Cannot transform segmentation of type '{}'!"321                "Supported types are: polygons as list[list[float] or ndarray],"322                " COCO-style RLE as a dict.".format(type(segm))323            )324 325    if "keypoints" in annotation:326        keypoints = transform_keypoint_annotations(327            annotation["keypoints"], transforms, image_size, keypoint_hflip_indices328        )329        annotation["keypoints"] = keypoints330 331    return annotation332 333 334def transform_keypoint_annotations(keypoints, transforms, image_size, keypoint_hflip_indices=None):335    """336    Transform keypoint annotations of an image.337    If a keypoint is transformed out of image boundary, it will be marked "unlabeled" (visibility=0)338 339    Args:340        keypoints (list[float]): Nx3 float in Detectron2's Dataset format.341            Each point is represented by (x, y, visibility).342        transforms (TransformList):343        image_size (tuple): the height, width of the transformed image344        keypoint_hflip_indices (ndarray[int]): see `create_keypoint_hflip_indices`.345            When `transforms` includes horizontal flip, will use the index346            mapping to flip keypoints.347    """348    # (N*3,) -> (N, 3)349    keypoints = np.asarray(keypoints, dtype="float64").reshape(-1, 3)350    keypoints_xy = transforms.apply_coords(keypoints[:, :2])351 352    # Set all out-of-boundary points to "unlabeled"353    inside = (keypoints_xy >= np.array([0, 0])) & (keypoints_xy <= np.array(image_size[::-1]))354    inside = inside.all(axis=1)355    keypoints[:, :2] = keypoints_xy356    keypoints[:, 2][~inside] = 0357 358    # This assumes that HorizFlipTransform is the only one that does flip359    do_hflip = sum(isinstance(t, T.HFlipTransform) for t in transforms.transforms) % 2 == 1360 361    # Alternative way: check if probe points was horizontally flipped.362    # probe = np.asarray([[0.0, 0.0], [image_width, 0.0]])363    # probe_aug = transforms.apply_coords(probe.copy())364    # do_hflip = np.sign(probe[1][0] - probe[0][0]) != np.sign(probe_aug[1][0] - probe_aug[0][0])  # noqa365 366    # If flipped, swap each keypoint with its opposite-handed equivalent367    if do_hflip:368        if keypoint_hflip_indices is None:369            raise ValueError("Cannot flip keypoints without providing flip indices!")370        if len(keypoints) != len(keypoint_hflip_indices):371            raise ValueError(372                "Keypoint data has {} points, but metadata "373                "contains {} points!".format(len(keypoints), len(keypoint_hflip_indices))374            )375        keypoints = keypoints[np.asarray(keypoint_hflip_indices, dtype=np.int32), :]376 377    # Maintain COCO convention that if visibility == 0 (unlabeled), then x, y = 0378    keypoints[keypoints[:, 2] == 0] = 0379    return keypoints380 381 382def annotations_to_instances(annos, image_size, mask_format="polygon"):383    """384    Create an :class:`Instances` object used by the models,385    from instance annotations in the dataset dict.386 387    Args:388        annos (list[dict]): a list of instance annotations in one image, each389            element for one instance.390        image_size (tuple): height, width391 392    Returns:393        Instances:394            It will contain fields "gt_boxes", "gt_classes",395            "gt_masks", "gt_keypoints", if they can be obtained from `annos`.396            This is the format that builtin models expect.397    """398    boxes = (399        np.stack(400            [BoxMode.convert(obj["bbox"], obj["bbox_mode"], BoxMode.XYXY_ABS) for obj in annos]401        )402        if len(annos)403        else np.zeros((0, 4))404    )405    target = Instances(image_size)406    target.gt_boxes = Boxes(boxes)407 408    classes = [int(obj["category_id"]) for obj in annos]409    classes = torch.tensor(classes, dtype=torch.int64)410    target.gt_classes = classes411 412    if len(annos) and "segmentation" in annos[0]:413        segms = [obj["segmentation"] for obj in annos]414        if mask_format == "polygon":415            try:416                masks = PolygonMasks(segms)417            except ValueError as e:418                raise ValueError(419                    "Failed to use mask_format=='polygon' from the given annotations!"420                ) from e421        else:422            assert mask_format == "bitmask", mask_format423            masks = []424            for segm in segms:425                if isinstance(segm, list):426                    # polygon427                    masks.append(polygons_to_bitmask(segm, *image_size))428                elif isinstance(segm, dict):429                    # COCO RLE430                    masks.append(mask_util.decode(segm))431                elif isinstance(segm, np.ndarray):432                    assert segm.ndim == 2, "Expect segmentation of 2 dimensions, got {}.".format(433                        segm.ndim434                    )435                    # mask array436                    masks.append(segm)437                else:438                    raise ValueError(439                        "Cannot convert segmentation of type '{}' to BitMasks!"440                        "Supported types are: polygons as list[list[float] or ndarray],"441                        " COCO-style RLE as a dict, or a binary segmentation mask "442                        " in a 2D numpy array of shape HxW.".format(type(segm))443                    )444            # torch.from_numpy does not support array with negative stride.445            masks = BitMasks(446                torch.stack([torch.from_numpy(np.ascontiguousarray(x)) for x in masks])447            )448        target.gt_masks = masks449 450    if len(annos) and "keypoints" in annos[0]:451        kpts = [obj.get("keypoints", []) for obj in annos]452        target.gt_keypoints = Keypoints(kpts)453 454    return target455 456 457def annotations_to_instances_rotated(annos, image_size):458    """459    Create an :class:`Instances` object used by the models,460    from instance annotations in the dataset dict.461    Compared to `annotations_to_instances`, this function is for rotated boxes only462 463    Args:464        annos (list[dict]): a list of instance annotations in one image, each465            element for one instance.466        image_size (tuple): height, width467 468    Returns:469        Instances:470            Containing fields "gt_boxes", "gt_classes",471            if they can be obtained from `annos`.472            This is the format that builtin models expect.473    """474    boxes = [obj["bbox"] for obj in annos]475    target = Instances(image_size)476    boxes = target.gt_boxes = RotatedBoxes(boxes)477    boxes.clip(image_size)478 479    classes = [obj["category_id"] for obj in annos]480    classes = torch.tensor(classes, dtype=torch.int64)481    target.gt_classes = classes482 483    return target484 485 486def filter_empty_instances(487    instances, by_box=True, by_mask=True, box_threshold=1e-5, return_mask=False488):489    """490    Filter out empty instances in an `Instances` object.491 492    Args:493        instances (Instances):494        by_box (bool): whether to filter out instances with empty boxes495        by_mask (bool): whether to filter out instances with empty masks496        box_threshold (float): minimum width and height to be considered non-empty497        return_mask (bool): whether to return boolean mask of filtered instances498 499    Returns:500        Instances: the filtered instances.501        tensor[bool], optional: boolean mask of filtered instances502    """503    assert by_box or by_mask504    r = []505    if by_box:506        r.append(instances.gt_boxes.nonempty(threshold=box_threshold))507    if instances.has("gt_masks") and by_mask:508        r.append(instances.gt_masks.nonempty())509 510    # TODO: can also filter visible keypoints511 512    if not r:513        return instances514    m = r[0]515    for x in r[1:]:516        m = m & x517    if return_mask:518        return instances[m], m519    return instances[m]520 521 522def create_keypoint_hflip_indices(dataset_names: Union[str, List[str]]) -> List[int]:523    """524    Args:525        dataset_names: list of dataset names526 527    Returns:528        list[int]: a list of size=#keypoints, storing the529        horizontally-flipped keypoint indices.530    """531    if isinstance(dataset_names, str):532        dataset_names = [dataset_names]533 534    check_metadata_consistency("keypoint_names", dataset_names)535    check_metadata_consistency("keypoint_flip_map", dataset_names)536 537    meta = MetadataCatalog.get(dataset_names[0])538    names = meta.keypoint_names539    # TODO flip -> hflip540    flip_map = dict(meta.keypoint_flip_map)541    flip_map.update({v: k for k, v in flip_map.items()})542    flipped_names = [i if i not in flip_map else flip_map[i] for i in names]543    flip_indices = [names.index(i) for i in flipped_names]544    return flip_indices545 546 547def get_fed_loss_cls_weights(dataset_names: Union[str, List[str]], freq_weight_power=1.0):548    """549    Get frequency weight for each class sorted by class id.550    We now calcualte freqency weight using image_count to the power freq_weight_power.551 552    Args:553        dataset_names: list of dataset names554        freq_weight_power: power value555    """556    if isinstance(dataset_names, str):557        dataset_names = [dataset_names]558 559    check_metadata_consistency("class_image_count", dataset_names)560 561    meta = MetadataCatalog.get(dataset_names[0])562    class_freq_meta = meta.class_image_count563    class_freq = torch.tensor(564        [c["image_count"] for c in sorted(class_freq_meta, key=lambda x: x["id"])]565    )566    class_freq_weight = class_freq.float() ** freq_weight_power567    return class_freq_weight568 569 570def gen_crop_transform_with_instance(crop_size, image_size, instance):571    """572    Generate a CropTransform so that the cropping region contains573    the center of the given instance.574 575    Args:576        crop_size (tuple): h, w in pixels577        image_size (tuple): h, w578        instance (dict): an annotation dict of one instance, in Detectron2's579            dataset format.580    """581    crop_size = np.asarray(crop_size, dtype=np.int32)582    bbox = BoxMode.convert(instance["bbox"], instance["bbox_mode"], BoxMode.XYXY_ABS)583    center_yx = (bbox[1] + bbox[3]) * 0.5, (bbox[0] + bbox[2]) * 0.5584    assert (585        image_size[0] >= center_yx[0] and image_size[1] >= center_yx[1]586    ), "The annotation bounding box is outside of the image!"587    assert (588        image_size[0] >= crop_size[0] and image_size[1] >= crop_size[1]589    ), "Crop size is larger than image size!"590 591    min_yx = np.maximum(np.floor(center_yx).astype(np.int32) - crop_size, 0)592    max_yx = np.maximum(np.asarray(image_size, dtype=np.int32) - crop_size, 0)593    max_yx = np.minimum(max_yx, np.ceil(center_yx).astype(np.int32))594 595    y0 = np.random.randint(min_yx[0], max_yx[0] + 1)596    x0 = np.random.randint(min_yx[1], max_yx[1] + 1)597    return T.CropTransform(x0, y0, crop_size[1], crop_size[0])598 599 600def check_metadata_consistency(key, dataset_names):601    """602    Check that the datasets have consistent metadata.603 604    Args:605        key (str): a metadata key606        dataset_names (list[str]): a list of dataset names607 608    Raises:609        AttributeError: if the key does not exist in the metadata610        ValueError: if the given datasets do not have the same metadata values defined by key611    """612    if len(dataset_names) == 0:613        return614    logger = logging.getLogger(__name__)615    entries_per_dataset = [getattr(MetadataCatalog.get(d), key) for d in dataset_names]616    for idx, entry in enumerate(entries_per_dataset):617        if entry != entries_per_dataset[0]:618            logger.error(619                "Metadata '{}' for dataset '{}' is '{}'".format(key, dataset_names[idx], str(entry))620            )621            logger.error(622                "Metadata '{}' for dataset '{}' is '{}'".format(623                    key, dataset_names[0], str(entries_per_dataset[0])624                )625            )626            raise ValueError("Datasets have different metadata '{}'!".format(key))627 628 629def build_augmentation(cfg, is_train):630    """631    Create a list of default :class:`Augmentation` from config.632    Now it includes resizing and flipping.633 634    Returns:635        list[Augmentation]636    """637    if is_train:638        min_size = cfg.INPUT.MIN_SIZE_TRAIN639        max_size = cfg.INPUT.MAX_SIZE_TRAIN640        sample_style = cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING641    else:642        min_size = cfg.INPUT.MIN_SIZE_TEST643        max_size = cfg.INPUT.MAX_SIZE_TEST644        sample_style = "choice"645    augmentation = [T.ResizeShortestEdge(min_size, max_size, sample_style)]646    if is_train and cfg.INPUT.RANDOM_FLIP != "none":647        augmentation.append(648            T.RandomFlip(649                horizontal=cfg.INPUT.RANDOM_FLIP == "horizontal",650                vertical=cfg.INPUT.RANDOM_FLIP == "vertical",651            )652        )653    return augmentation654 655 656build_transform_gen = build_augmentation657"""658Alias for backward-compatibility.659"""660