示例#1
0
    def test_produces_valid_crop(self):
        """
        <b>Description:</b>
        Checks that shape_produces_valid_crop returns the correct values and
        does not raise errors

        <b>Input data:</b>
        A valid Rectangle at [0, 0.4, 1, 0.5]
        A Polygon that has an invalid bounding box

        <b>Expected results:</b>
        The test passes if the call with the Rectangle returns True and
        the one with the polygon returns False

        <b>Steps</b>
        1. Check Valid Rectangle
        2. Check invalid Polygon
        """
        rectangle = Rectangle(x1=0, y1=0.4, x2=1, y2=0.5)
        assert ShapeFactory.shape_produces_valid_crop(rectangle, 100, 100)

        point1 = Point(x=0.01, y=0.1)
        point2 = Point(x=0.35, y=0.1)
        point3 = Point(x=0.35, y=0.1)
        polygon = Polygon(points=[point1, point2, point3])
        assert not ShapeFactory.shape_produces_valid_crop(polygon, 100, 250)
    def append_annotations(self, annotations: Sequence[Annotation]):
        """
        Adds a list of shapes to the annotation
        """
        roi_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape)

        validated_annotations = [
            Annotation(
                shape=annotation.shape.normalize_wrt_roi_shape(roi_as_box),
                labels=annotation.get_labels(),
            ) for annotation in annotations
            if ShapeFactory().shape_produces_valid_crop(
                shape=annotation.shape,
                media_width=self.media.width,
                media_height=self.media.height,
            )
        ]

        n_invalid_shapes = len(annotations) - len(validated_annotations)
        if n_invalid_shapes > 0:
            logger.info(
                "%d shapes will not be added to the dataset item as they "
                "would produce invalid crops (this is expected for some tasks, "
                "such as segmentation).",
                n_invalid_shapes,
            )

        self.annotation_scene.append_annotations(validated_annotations)
    def __get_boxes_from_dataset_as_list(dataset: DatasetEntity,
                                         labels: List[LabelEntity]) -> List:
        """
        Explanation of output shape:
            a box: [x1: float, y1, x2, y2, class: str, score: float]
            boxes_per_image: [box1, box2, …]
            ground_truth_boxes_per_image: [boxes_per_image_1, boxes_per_image_2, boxes_per_image_3, …]

        :param dataset:
        :param labels: significant labels for detection task
        :return: returns list with shape: List[List[List[Optional[float, str]]]]
        """
        boxes_per_image = []
        converted_types_to_box = set()
        label_names = {label.name for label in labels}
        for item in dataset:
            boxes: List[List[Union[float, str]]] = []
            roi_as_box = Annotation(ShapeFactory.shape_as_rectangle(
                item.roi.shape),
                                    labels=[])
            for annotation in item.annotation_scene.annotations:
                shape_as_box = ShapeFactory.shape_as_rectangle(
                    annotation.shape)
                box = shape_as_box.normalize_wrt_roi_shape(roi_as_box.shape)
                n_boxes_before = len(boxes)
                boxes.extend([[
                    box.x1, box.y1, box.x2, box.y2, label.name,
                    label.probability
                ] for label in annotation.get_labels()
                              if label.name in label_names])
                if (not isinstance(annotation.shape, Rectangle)
                        and len(boxes) > n_boxes_before):
                    converted_types_to_box.add(
                        annotation.shape.__class__.__name__)
            boxes_per_image.append(boxes)
        if len(converted_types_to_box) > 0:
            logger.warning(
                f"The shapes of types {tuple(converted_types_to_box)} have been converted to their "
                f"full enclosing Box representation in order to compute the f-measure"
            )

        return boxes_per_image
    def get_annotations(
        self,
        labels: Optional[List[LabelEntity]] = None,
        include_empty: bool = False,
        ios_threshold: float = 0.0,
    ) -> List[Annotation]:
        """
        Returns a list of annotations that exist in the dataset item (wrt. ROI)

        :param labels: Subset of input labels to filter with; if ``None``, all the shapes within the ROI are returned
        :param include_empty: if True, returns both empty and non-empty labels
        :param ios_threshold: Only return shapes where Area(self.roi ∩ shape)/ Area(shape) > ios_threshold.
        :return: The intersection of the input label set and those present within the ROI
        """
        is_full_box = Rectangle.is_full_box(self.roi.shape)
        annotations = []
        if is_full_box and labels is None and not include_empty:
            # Fast path for the case where we do not need to change the shapes
            annotations = self.annotation_scene.annotations
        else:
            # Todo: improve speed. This is O(n) for n shapes.
            roi_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape)

            labels_set = {label.name
                          for label in labels} if labels is not None else {}

            for annotation in self.annotation_scene.annotations:
                if (not is_full_box and self.roi.shape.intersect_percentage(
                        annotation.shape) <= ios_threshold):
                    continue

                shape_labels = annotation.get_labels(include_empty)

                if labels is not None:
                    shape_labels = [
                        label for label in shape_labels
                        if label.name in labels_set
                    ]

                    if len(shape_labels) == 0:
                        continue

                if not is_full_box:
                    # Create a denormalized copy of the shape.
                    shape = annotation.shape.denormalize_wrt_roi_shape(
                        roi_as_box)
                else:
                    # Also create a copy of the shape, so that we can safely modify the labels
                    # without tampering with the original shape.
                    shape = copy.deepcopy(annotation.shape)

                annotations.append(Annotation(shape=shape,
                                              labels=shape_labels))
        return annotations
示例#5
0
    def test_polygon_shape_conversion(self):
        """
        <b>Description:</b>
        Checks that conversions from Polygon to other shapes works correctly

        <b>Input data:</b>
        A Polygon at [[0.01, 0.2], [0.35, 0.2], [0.35, 0.4]]

        <b>Expected results:</b>
        The test passes if the Polygon can be converted to Rectangle and Ellipse

        <b>Steps</b>
        1. Create rectangle and get coordinates
        2. Convert to Ellipse
        3. Convert to Polygon
        4. Convert to Rectangle
        """
        point1 = Point(x=0.01, y=0.2)
        point2 = Point(x=0.35, y=0.2)
        point3 = Point(x=0.35, y=0.4)
        polygon = Polygon(points=[point1, point2, point3])
        polygon_coords = (polygon.min_x, polygon.min_y, polygon.max_x,
                          polygon.max_y)

        ellipse = ShapeFactory.shape_as_ellipse(polygon)
        assert isinstance(ellipse, Ellipse)
        assert (ellipse.x1, ellipse.y1, ellipse.x2,
                ellipse.y2) == polygon_coords

        polygon2 = ShapeFactory.shape_as_polygon(polygon)
        assert isinstance(polygon2, Polygon)
        assert polygon == polygon2

        rectangle = ShapeFactory.shape_as_rectangle(polygon)
        assert isinstance(rectangle, Rectangle)
        assert (
            rectangle.x1,
            rectangle.y1,
            rectangle.x2,
            rectangle.y2,
        ) == polygon_coords
示例#6
0
    def test_ellipse_shape_conversion(self):
        """
        <b>Description:</b>
        Checks that conversions from Ellipse to other shapes works correctly

        <b>Input data:</b>
        A rectangle at [0.1, 0.1, 0.5, 0.2]

        <b>Expected results:</b>
        The test passes if the Ellipse can be converted to Rectangle and Polygon

        <b>Steps</b>
        1. Create Ellipse and get coordinates
        2. Convert to Ellipse
        3. Convert to Polygon
        4. Convert to Rectangle
        """
        ellipse = Ellipse(x1=0.1, y1=0.1, x2=0.5, y2=0.2)
        ellipse_coords = (ellipse.x1, ellipse.y1, ellipse.x2, ellipse.y2)

        ellipse2 = ShapeFactory.shape_as_ellipse(ellipse)
        assert isinstance(ellipse, Ellipse)
        assert ellipse == ellipse2

        polygon = ShapeFactory.shape_as_polygon(ellipse)
        assert isinstance(polygon, Polygon)
        assert polygon.min_x == pytest.approx(ellipse.x1, 0.1)
        assert polygon.min_y == pytest.approx(ellipse.y1, 0.1)
        assert polygon.max_x == pytest.approx(ellipse.x2, 0.1)
        assert polygon.max_y == pytest.approx(ellipse.y2, 0.1)

        rectangle = ShapeFactory.shape_as_rectangle(ellipse)
        assert isinstance(rectangle, Rectangle)
        assert (
            rectangle.x1,
            rectangle.y1,
            rectangle.x2,
            rectangle.y2,
        ) == ellipse_coords
    def height(self) -> int:
        """
        The height of the dataset item, taking into account the ROI.
        """
        roi_shape_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape)
        roi_shape_as_box = roi_shape_as_box.clip_to_visible_region()
        height = self.media.height

        # Note that we cannot directly use roi_shape_as_box.height due to the rounding
        # because round(y2 - y1) is not always equal to round(y2) - round(y1)
        y1 = int(round(roi_shape_as_box.y1 * height))
        y2 = int(round(roi_shape_as_box.y2 * height))
        return y2 - y1
    def width(self) -> int:
        """
        The width of the dataset item, taking into account the ROI.
        """
        roi_shape_as_box = ShapeFactory.shape_as_rectangle(self.roi.shape)
        roi_shape_as_box = roi_shape_as_box.clip_to_visible_region()
        width = self.media.width

        # Note that we cannot directly use roi_shape_as_box.width due to the rounding
        # because round(x2 - x1) is not always equal to round(x2) - round(x1)
        x1 = int(round(roi_shape_as_box.x1 * width))
        x2 = int(round(roi_shape_as_box.x2 * width))
        return x2 - x1
示例#9
0
    def test_rectangle_shape_conversion(self):
        """
        <b>Description:</b>
        Checks that conversions from Rectangle to other shapes works correctly

        <b>Input data:</b>
        A rectangle at [0.25, 0.1, 0.5, 0.3]

        <b>Expected results:</b>
        The test passes if the rectangle can be converted to Ellipse and Polygon

        <b>Steps</b>
        1. Create rectangle and get coordinates
        2. Convert to Ellipse
        3. Convert to Polygon
        4. Convert to Rectangle
        """
        rectangle = Rectangle(x1=0.25, y1=0.1, x2=0.5, y2=0.3)
        rectangle_coords = (rectangle.x1, rectangle.y1, rectangle.x2,
                            rectangle.y2)

        ellipse = ShapeFactory.shape_as_ellipse(rectangle)
        assert isinstance(ellipse, Ellipse)
        assert (ellipse.x1, ellipse.y1, ellipse.x2,
                ellipse.y2) == rectangle_coords

        polygon = ShapeFactory.shape_as_polygon(rectangle)
        assert isinstance(polygon, Polygon)
        assert (
            polygon.min_x,
            polygon.min_y,
            polygon.max_x,
            polygon.max_y,
        ) == rectangle_coords

        rectangle2 = ShapeFactory.shape_as_rectangle(rectangle)
        assert isinstance(rectangle2, Rectangle)
        assert rectangle == rectangle2
    def roi_numpy(self, roi: Optional[Annotation] = None) -> np.ndarray:
        """
        Gives the numpy data for the media, given an ROI.
        This function allows to take a crop of any arbitrary region of the media in the Dataset entity.
        If the ROI is not given, the ROI assigned to the DatasetItem will be used as default.

        :param roi: Shape entity. The shape will be converted if needed, to extract the ROI numpy.

        :return: Numpy array with media data
        """
        if roi is None:
            roi = self.roi

        if roi is not None:
            roi.shape = ShapeFactory.shape_as_rectangle(roi.shape)

        return self.media.roi_numpy(roi=roi)
示例#11
0
def mask_from_annotation(annotations: List[Annotation],
                         labels: List[LabelEntity], width: int,
                         height: int) -> np.ndarray:
    """
    Generate a segmentation mask of a numpy image, and a list of shapes.
    The mask is will be two dimensional    and the value of each pixel matches the class
    index with offset 1. The background class index is zero.  labels[0] matches pixel
    value 1, etc. The class index is determined based on the order of :param labels:

    :param annotations: List of annotations to plot in mask
    :param labels: List of labels. The index position of the label determines the class
                   number in the segmentation mask.
    :param width: Width of the mask
    :param height: Height of the mask

    :return: 2d numpy array of mask
    """

    labels = sorted(labels)  # type: ignore
    mask = np.zeros(shape=(height, width), dtype=np.uint8)
    for annotation in annotations:
        shape = annotation.shape
        if not isinstance(shape, Polygon):
            shape = ShapeFactory.shape_as_polygon(annotation.shape)
        known_labels = [
            label for label in annotation.get_labels()
            if isinstance(label, ScoredLabel) and label.get_label() in labels
        ]
        if len(known_labels) == 0:
            # Skip unknown shapes
            continue

        label_to_compare = known_labels[0].get_label()

        class_idx = labels.index(label_to_compare) + 1
        contour = []
        for point in shape.points:
            contour.append([int(point.x * width), int(point.y * height)])

        mask = cv2.drawContours(mask, np.asarray([contour]), 0,
                                (class_idx, class_idx, class_idx), -1)

    mask = np.expand_dims(mask, axis=2)

    return mask