diff --git a/darwin/datatypes.py b/darwin/datatypes.py index 30a02bb00..5b79c7958 100644 --- a/darwin/datatypes.py +++ b/darwin/datatypes.py @@ -713,6 +713,58 @@ def make_polygon( ) +def make_complex_polygon( + class_name: str, + point_paths: List[List[Point]], + bounding_box: Optional[Dict] = None, + subs: Optional[List[SubAnnotation]] = None, + slot_names: Optional[List[str]] = None, +) -> Annotation: + """ + Creates and returns a complex polygon annotation. Complex polygons are those who have holes + and/or disform shapes. This is used by the backend. + + Parameters + ---------- + class_name: str + The name of the class for this ``Annotation``. + point_paths: List[List[Point]] + A list of lists points that comprises the complex polygon. This is needed as a complex + polygon can be effectively seen as a sum of multiple simple polygons. The list should have + a format similar to: + + .. code-block:: python + + [ + [ + {"x": 1, "y": 0}, + {"x": 2, "y": 1} + ], + [ + {"x": 3, "y": 4}, + {"x": 5, "y": 6} + ] + # ... and so on ... + ] + + bounding_box : Optional[Dict], default: None + The bounding box that encompasses the polyong. + subs : Optional[List[SubAnnotation]], default: None + List of ``SubAnnotation``s for this ``Annotation``. + + Returns + ------- + Annotation + A complex polygon ``Annotation``. + """ + return Annotation( + AnnotationClass(class_name, "complex_polygon", "polygon"), + _maybe_add_bounding_box_data({"paths": point_paths}, bounding_box), + subs or [], + slot_names=slot_names or [], + ) + + def make_keypoint( class_name: str, x: float, diff --git a/darwin/utils/utils.py b/darwin/utils/utils.py index 05a36aa2c..1d5789fa7 100644 --- a/darwin/utils/utils.py +++ b/darwin/utils/utils.py @@ -1449,3 +1449,58 @@ def _default_schema(version: dt.AnnotationFileVersion) -> Optional[str]: return _supported_schema_versions().get( (version.major, version.minor, version.suffix) ) + + +def convert_sequences_to_polygons( + sequences: List[Union[List[int], List[float]]], + height: Optional[int] = None, + width: Optional[int] = None, +) -> Dict[str, List[dt.Polygon]]: + """ + Converts a list of polygons, encoded as a list of dictionaries of into a list of nd.arrays + of coordinates. This is used by the backend. + + Parameters + ---------- + sequences : List[Union[List[int], List[float]]] + List of arrays of coordinates in the format ``[x1, y1, x2, y2, ..., xn, yn]`` or as a list + of them as ``[[x1, y1, x2, y2, ..., xn, yn], ..., [x1, y1, x2, y2, ..., xn, yn]]``. + height : Optional[int], default: None + Maximum height for a polygon coordinate. + width : Optional[int], default: None + Maximum width for a polygon coordinate. + + Returns + ------- + Dict[str, List[dt.Polygon]] + Dictionary with the key ``path`` containing a list of coordinates in the format of + ``[[{x: x1, y:y1}, ..., {x: xn, y:yn}], ..., [{x: x1, y:y1}, ..., {x: xn, y:yn}]]``. + + Raises + ------ + ValueError + If sequences is a falsy value (such as ``[]``) or if it is in an incorrect format. + """ + if not sequences: + raise ValueError("No sequences provided") + # If there is a single sequences composing the instance then this is + # transformed to polygons = [[x1, y1, ..., xn, yn]] + if not isinstance(sequences[0], list): + sequences = [sequences] + + if not isinstance(sequences[0][0], (int, float)): + raise ValueError("Unknown input format") + + def grouped(iterable, n): + return zip(*[iter(iterable)] * n) + + polygons = [] + for sequence in sequences: + path = [] + for x, y in grouped(sequence, 2): + # Clip coordinates to the image size + x = max(min(x, width - 1) if width else x, 0) + y = max(min(y, height - 1) if height else y, 0) + path.append({"x": x, "y": y}) + polygons.append(path) + return {"path": polygons}