Source code for playground_metrics.utils.conversion

from collections.abc import Sized

import numpy as np
from pygeos import linearrings, polygons, points, box, is_valid, make_valid, get_num_geometries, get_geometry


# Helpers functions
from playground_metrics.utils.geometry import is_type, GeometryType


[docs]def get_type_and_convert(input_array, trim_invalid_geometry=False, autocorrect_invalid_geometry=False): r"""Automatically find the geometry type from the input array shape and convert it to a geometry array. Args: input_array (ndarray, list): * A ndarray of detections stored as: * Bounding boxes for a given class where each row is a detection stored as: ``[x_min, y_min, x_max, y_max, confidence, label]`` * Polygons for a given class where each row is a detection stored as: ``[[[outer_ring], [inner_rings]], confidence, label]`` * Points for a given class where each row is a detection stored as: ``[x, y, confidence, label]`` * A ndarray of ground truth stored as: * Bounding boxes for a given class where each row is a ground truth stored as: ``[x_min, y_min, x_max, y_max, label]`` * Polygons for a given class where each row is a ground truth stored as: ``[[[outer_ring], [inner_rings]], label]`` * Points for a given class where each row is a ground truth stored as: ``[x, y, label]`` trim_invalid_geometry (bool): Optional, default to ``False``. If set to ``True`` conversion will ignore invalid geometries and leave them out of ``output_array``. This means that the function will return an array where ``output_array.shape[0] <= input_array.shape[0]``. If set to ``False``, an invalid geometry will raise an :exc:`~playground_metrics.utils.geometry_utils.InvalidGeometryError`. autocorrect_invalid_geometry (Bool): Optional, default to ``False``. Whether to attempt correcting a faulty geometry to form a valid one. If set to ``True`` and the autocorrect attempt is unsuccessful, it falls back to the behaviour defined in ``trim_invalid_geometry``. Note: * Polygon auto-correction only corrects self-crossing exterior rings, in which case it creates one Polygon out of every simple ring which might be extracted from the original Polygon exterior. * Polygon auto-correction will systematically fail on Polygons with at least one inner ring. Returns: (str, ndarray): A tuple of output containing: * The BaseGeometry type as a string which may be either ``"point"``, ``"polygon"`` or ``"bbox"`` * A geometry ndarray where each row contains a geometry followed by optionally confidence and a label e.g.: ``[BaseGeometry, (confidence), label]`` Raises: ValueError: If ``input_array`` have invalid dimensions. """ input_array = np.array(input_array, dtype=np.dtype('O')) # If nothing pass here if input_array.size == 0: return 'undefined', input_array if len(input_array.shape) < 2: # If we have less than 2 dimensions, pull the plug its useless raise ValueError('Invalid array number of dimensions: ' 'Expected a 2D array, found {}D.'.format(len(input_array.shape))) elif len(input_array.shape) == 2: # It might just be anything at this point. We need to check the second dimension value to decide if input_array.shape[1] < 2: # If it's less than 2, pull the plug its useless raise ValueError('Invalid array second dimension: ' 'Expected at least 2, found {}.'.format(input_array.shape[1])) elif input_array.shape[1] == 2: # It's a Polygon ndarray type_ = 'polygon' # Convert rings to Polygon input_array = convert_to_polygon(input_array, trim_invalid_geometry=trim_invalid_geometry, autocorrect_invalid_geometry=autocorrect_invalid_geometry) elif input_array.shape[1] == 3: # It might be either a Polygon ndarray or a Point ndarray # We check the first element to decide. One might argue that it is not robust to a mixed-type input array, # that is true but the conversion functions implicitly assumes that the input array is of fixed-type for # performances issues. if isinstance(input_array[0, 0], Sized): # Reasonable guess is the first element is a list or a ndarray -> It's a Polygon ndarray type_ = 'polygon' # Convert rings to Polygon input_array = convert_to_polygon(input_array, trim_invalid_geometry=trim_invalid_geometry, autocorrect_invalid_geometry=autocorrect_invalid_geometry) else: # Reasonable guess is the first element is a number -> It's a Point ndarray type_ = 'point' input_array = convert_to_point(input_array, trim_invalid_geometry=trim_invalid_geometry) elif input_array.shape[1] == 4: # It's a Point ndarray type_ = 'point' input_array = convert_to_point(input_array, trim_invalid_geometry=trim_invalid_geometry) elif 7 > input_array.shape[1] > 4: # It's a BoundingBox ndarray type_ = 'bbox' input_array = convert_to_bounding_box(input_array, trim_invalid_geometry=trim_invalid_geometry) else: raise ValueError('Invalid array second dimension: ' 'Expected less than 6, found {}.'.format(input_array.shape[1])) elif len(input_array.shape) == 3: # It's the weirdest Polygon and tuple class corner case ! This really is not a drill ! # It's a Polygon ndarray type_ = 'polygon' # Convert rings to Polygon input_array = convert_to_polygon(input_array, trim_invalid_geometry=trim_invalid_geometry, autocorrect_invalid_geometry=autocorrect_invalid_geometry) elif len(input_array.shape) == 5: # It's the mildly weird Polygon corner case ! This is not a drill ! # It's a Polygon ndarray type_ = 'polygon' # Convert rings to Polygon input_array = convert_to_polygon(input_array, trim_invalid_geometry=trim_invalid_geometry, autocorrect_invalid_geometry=autocorrect_invalid_geometry) else: # If we have neither 2 dimensions nor 5 (in a weird polygon corner case), pull the plug its useless raise ValueError('Invalid array number of dimensions: ' 'Expected a 2D array, found {}D.'.format(len(input_array.shape))) return type_, input_array
def _clean_multi_geometries(object_array): """Cleanup a sequence of geometries to remove multi geometries. Args: object_array (numpy.ndarray): The object array to cleanup Returns: numpy.ndarray: Cleaned-up object array. """ # Handle multi-geometries geometries = object_array[:, 0] num_geometries = get_num_geometries(geometries) for index in np.nonzero(num_geometries > 1)[0]: split_geometries = [np.concatenate((get_geometry(geometries[index], i), object_array[index, 1:])) for i in range(num_geometries[index])] object_array[index] = split_geometries[0] object_array = np.concatenate((object_array, split_geometries[1:, :])) return geometries
[docs]def convert_to_polygon(input_array, trim_invalid_geometry=False, autocorrect_invalid_geometry=False): r"""Convert an input array to a Polygon array. Args: input_array (ndarray, list): A ndarray of Polygons optionally followed by a confidence value and/or a label where each row is: ``[[[outer_ring], [inner_rings]], (confidence), (label)]`` trim_invalid_geometry (bool): Optional, default to ``False``. If set to ``True`` conversion will ignore invalid geometries and leave them out of ``output_array``. This means that the function will return an array where ``output_array.shape[0] <= input_array.shape[0]``. If set to ``False``, an invalid geometry will raise an :exc:`~playground_metrics.utils.geometry_utils.InvalidGeometryError`. autocorrect_invalid_geometry (Bool): Optional, default to ``False``. Whether to attempt correcting a faulty geometry to form a valid one. If set to ``True`` and the autocorrect attempt is unsuccessful, it falls back to the behaviour defined in ``trim_invalid_geometry``. Note: * Polygon auto-correction only corrects self-crossing exterior rings, in which case it creates one Polygon out of every simple ring which might be extracted from the original Polygon exterior. * Polygon auto-correction will systematically fail on Polygons with at least one inner ring. Returns: ndarray: A Polygon ndarray where each row contains a geometry followed by optionally confidence and a label e.g.: ``[Polygon, (confidence), (label)]`` Raises: ValueError: If ``input_array`` have invalid dimensions. """ input_array = np.array(input_array, dtype=np.dtype('O')) if input_array.size == 0: return 'undefined', input_array if (len(input_array.shape) == 1 or len(input_array.shape) > 2) and \ (not len(input_array.shape) == 5 and not len(input_array.shape) == 3): raise ValueError('Invalid array number of dimensions: ' 'Expected a 2D array, found {}D.'.format(len(input_array.shape))) if len(input_array.shape) == 5 and not input_array.shape[4] == 2: raise ValueError('Invalid array fifth dimension: ' 'Expected 2, found {}.'.format(len(input_array.shape))) elif len(input_array.shape) == 3 and not input_array.shape[2] == 1: raise ValueError('Invalid array third dimension: ' 'Expected 1, found {}.'.format(len(input_array.shape))) object_array = np.ndarray((input_array.shape[0], input_array.shape[1]), dtype=np.dtype('O')) for i, coordinate in enumerate(input_array[:, 0]): line = [polygons(linearrings(coordinate[0]), holes=[linearrings(hole) for hole in coordinate[1:]])] \ if len(coordinate) > 1 else [polygons(linearrings(coordinate[0]))] line.extend(input_array[i, 1:]) object_array[i] = np.array(line, dtype=np.dtype('O')) if autocorrect_invalid_geometry: object_array[:, 0] = _clean_multi_geometries(make_valid(object_array[:, 0])) if trim_invalid_geometry: object_array = object_array[is_valid(object_array[:, 0]), :] if not np.all(is_type(object_array[:, 0], GeometryType.POLYGON)): raise ValueError('Conversion is impossible: Some geometries could not be converted to valid polygons.') return object_array
[docs]def convert_to_bounding_box(input_array, trim_invalid_geometry=False, autocorrect_invalid_geometry=False): r"""Convert an input array to a BoundingBox array. Args: input_array (ndarray, list): A ndarray of BoundingBox optionally followed by a confidence value and/or a label where each row is: ``[xmin, ymin, xmax, ymax, (confidence), (label)]`` trim_invalid_geometry (bool): Optional, default to ``False``. If set to ``True`` conversion will ignore invalid geometries and leave them out of ``output_array``. This means that the function will return an array where ``output_array.shape[0] <= input_array.shape[0]``. If set to ``False``, an invalid geometry will raise an :exc:`~playground_metrics.utils.geometry_utils.InvalidGeometryError`. autocorrect_invalid_geometry (Bool): Optional, default to ``False``. Doesn't do anything, introduced to unify convert functions interfaces. Returns: ndarray: A BoundingBox ndarray where each row contains a geometry followed by optionally confidence and a label e.g.: ``[BoundingBox, (confidence), (label)]`` Raises: ValueError: If ``input_array`` have invalid dimensions. """ input_array = np.array(input_array, dtype=np.dtype('O')) if input_array.size == 0: return 'undefined', input_array if len(input_array.shape) == 1 or len(input_array.shape) > 2: raise ValueError('Invalid array number of dimensions: ' 'Expected a 2D array, found {}D.'.format(len(input_array.shape))) coordinates_array = input_array[:, :4].astype(np.float64) object_array = np.ndarray((input_array.shape[0], input_array.shape[1] - 3), dtype=np.dtype('O')) object_array[:, 0] = box(coordinates_array[:, 0], coordinates_array[:, 1], coordinates_array[:, 2], coordinates_array[:, 3]) object_array[:, 1:] = input_array[:, 4:] if trim_invalid_geometry: object_array = object_array[is_valid(object_array[:, 0]), :] return object_array
[docs]def convert_to_point(input_array, trim_invalid_geometry=False, autocorrect_invalid_geometry=False): r"""Convert an input array to a Point array. Args: input_array (ndarray, list): A ndarray of Point optionally followed by a confidence value and/or a label where each row is: ``[x, y, (confidence), (label)]`` trim_invalid_geometry (bool): Optional, default to ``False``. If set to ``True`` conversion will ignore invalid geometries and leave them out of ``output_array``. This means that the function will return an array where ``output_array.shape[0] <= input_array.shape[0]``. If set to ``False``, an invalid geometry will raise an :exc:`~playground_metrics.utils.geometry_utils.InvalidGeometryError`. autocorrect_invalid_geometry (Bool): Optional, default to ``False``. Doesn't do anything, introduced to unify convert functions interfaces. Returns: ndarray: A Point ndarray where each row contains a geometry followed by optionally confidence and a label e.g.: ``[Point, (confidence), (label)]`` Raises: ValueError: If ``input_array`` have invalid dimensions. """ input_array = np.array(input_array, dtype=np.dtype('O')) if input_array.size == 0: return 'undefined', input_array if len(input_array.shape) == 1 or len(input_array.shape) > 2: raise ValueError('Invalid array number of dimensions: ' 'Expected a 2D array, found {}D.'.format(len(input_array.shape))) coordinates_array = input_array[:, :2].astype(np.float64) object_array = np.ndarray((input_array.shape[0], input_array.shape[1] - 1), dtype=np.dtype('O')) object_array[:, 0] = points(coordinates_array) object_array[:, 1:] = input_array[:, 2:] if trim_invalid_geometry: object_array = object_array[is_valid(object_array[:, 0]), :] return object_array