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
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
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
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)
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