Skip to content

Label utils module

The module kili.utils.labels provides a set of helpers to convert point, bounding box, polygon and segmentation labels.

Info

In Kili json response format, a normalized vertex is a dictionary with keys x and y and values between 0 and 1. The origin is always the top left corner of the image. The x-axis is horizontal and the y-axis is vertical with the y-axis pointing down. You can find more information about the Kili data format here.

Points

kili.utils.labels.point

Helpers to create point annotations.

normalized_point_to_point(point, img_width=None, img_height=None, origin_location='bottom_left')

Convert a Kili normalized vertex to a 2D point.

It is the inverse of the method point_to_normalized_point.

A point is a dict with keys "x" and "y", and corresponding values in pixels (int or float).

Conventions for the input point:

  • The origin is the top left corner of the image.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical and goes from top to bottom.

Conventions for the output point:

  • The origin is defined by the origin_location argument.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical. If origin_location is "top_left", it goes from top to bottom. If origin_location is "bottom_left", it goes from bottom to top.

If the image width and height are provided, the output point coordinates will be scaled to the image size. If not, the method will return a point with normalized coordinates.

Parameters:

Name Type Description Default
point dict

Point to convert.

required
img_width Union[int, float]

Width of the image the point is defined in.

None
img_height Union[int, float]

Height of the image the point is defined in.

None
origin_location Literal['top_left', 'bottom_left']

Location of the origin of output point coordinate system. Can be either top_left or bottom_left.

'bottom_left'

Returns:

Type Description
dict

A dict with keys "x" and "y", and corresponding values in pixels.

Source code in kili/utils/labels/point.py
def normalized_point_to_point(
    point: dict[str, float],
    img_width: Optional[Union[int, float]] = None,
    img_height: Optional[Union[int, float]] = None,
    origin_location: Literal["top_left", "bottom_left"] = "bottom_left",
) -> dict[Literal["x", "y"], float]:
    # pylint: disable=line-too-long
    """Convert a Kili normalized vertex to a 2D point.

    It is the inverse of the method `point_to_normalized_point`.

    A point is a dict with keys `"x"` and `"y"`, and corresponding values in pixels (`int` or `float`).

    Conventions for the input point:

    - The origin is the top left corner of the image.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical and goes from top to bottom.

    Conventions for the output point:

    - The origin is defined by the `origin_location` argument.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical. If `origin_location` is `"top_left"`, it goes from top to bottom. If `origin_location` is `"bottom_left"`, it goes from bottom to top.

    If the image width and height are provided, the output point coordinates will be scaled to the image size.
    If not, the method will return a point with normalized coordinates.

    Args:
        point: Point to convert.
        img_width: Width of the image the point is defined in.
        img_height: Height of the image the point is defined in.
        origin_location: Location of the origin of output point coordinate system. Can be either `top_left` or `bottom_left`.

    Returns:
        A dict with keys `"x"` and `"y"`, and corresponding values in pixels.
    """
    if (img_width is None) != (img_height is None):
        raise ValueError("img_width and img_height must be both None or both not None.")

    if origin_location == "bottom_left":
        point = {"x": point["x"], "y": 1 - point["y"]}

    img_height = img_height or 1
    img_width = img_width or 1

    return {"x": point["x"] * img_width, "y": point["y"] * img_height}

point_to_normalized_point(point, img_width=None, img_height=None, origin_location='bottom_left')

Converts a 2D point to a Kili normalized vertex.

The output can be used to create object detection annotations. See the documentation for more details.

A point is a dict with keys "x" and "y", and corresponding values in pixels (int or float).

Conventions for the input point:

  • The origin is defined by the origin_location argument.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical. If origin_location is "top_left", it goes from top to bottom. If origin_location is "bottom_left", it goes from bottom to top.

Conventions for the output point:

  • The origin is the top left corner of the image.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical and goes from top to bottom.

If the image width and height are provided, the input point coordinates will be normalized to [0, 1]. If not, the method expects the input point coordinates to be already normalized.

Parameters:

Name Type Description Default
point dict

Point to convert.

required
img_width Union[int, float]

Width of the image the point is defined in.

None
img_height Union[int, float]

Height of the image the point is defined in.

None
origin_location Literal['top_left', 'bottom_left']

Location of the origin of input point coordinate system. Can be either top_left or bottom_left.

'bottom_left'

Returns:

Type Description
dict

A dict with keys "x" and "y", and corresponding normalized values.

Example

from kili.utils.labels.point import point_to_normalized_point

normalized_point = point_to_normalized_point({"x": 5, "y": 40}, img_width=100, img_height=100)

json_response = {
    "OBJECT_DETECTION_JOB": {
        "annotations": [
            {
                "point": normalized_point,
                "categories": [{"name": "CLASS_A"}],
                "type": "marker",
            }
        ]
    }
}
Source code in kili/utils/labels/point.py
def point_to_normalized_point(
    point: dict[str, Union[int, float]],
    img_width: Optional[Union[int, float]] = None,
    img_height: Optional[Union[int, float]] = None,
    origin_location: Literal["top_left", "bottom_left"] = "bottom_left",
) -> dict[Literal["x", "y"], float]:
    # pylint: disable=line-too-long
    """Converts a 2D point to a Kili normalized vertex.

    The output can be used to create object detection annotations. See the [documentation](https://docs.kili-technology.com/reference/export-object-entity-detection-and-relation) for more details.

    A point is a dict with keys `"x"` and `"y"`, and corresponding values in pixels (`int` or `float`).

    Conventions for the input point:

    - The origin is defined by the `origin_location` argument.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical. If `origin_location` is `"top_left"`, it goes from top to bottom. If `origin_location` is `"bottom_left"`, it goes from bottom to top.

    Conventions for the output point:

    - The origin is the top left corner of the image.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical and goes from top to bottom.

    If the image width and height are provided, the input point coordinates will be normalized to `[0, 1]`.
    If not, the method expects the input point coordinates to be already normalized.

    Args:
        point: Point to convert.
        img_width: Width of the image the point is defined in.
        img_height: Height of the image the point is defined in.
        origin_location: Location of the origin of input point coordinate system. Can be either `top_left` or `bottom_left`.

    Returns:
        A dict with keys `"x"` and `"y"`, and corresponding normalized values.

    !!! Example
        ```python
        from kili.utils.labels.point import point_to_normalized_point

        normalized_point = point_to_normalized_point({"x": 5, "y": 40}, img_width=100, img_height=100)

        json_response = {
            "OBJECT_DETECTION_JOB": {
                "annotations": [
                    {
                        "point": normalized_point,
                        "categories": [{"name": "CLASS_A"}],
                        "type": "marker",
                    }
                ]
            }
        }
        ```
    """
    if (img_width is None) != (img_height is None):
        raise ValueError("img_width and img_height must be both None or both not None.")

    if img_width is not None and img_height is not None:
        point = {
            "x": point["x"] / img_width,
            "y": point["y"] / img_height,
        }

    if origin_location == "bottom_left":
        point = {"x": point["x"], "y": 1 - point["y"]}

    assert 0 <= point["x"] <= 1, f"Point x coordinate {point['x']} should be in [0, 1]."
    assert 0 <= point["y"] <= 1, f"Point y coordinate {point['y']} should be in [0, 1]."

    return {"x": point["x"], "y": point["y"]}

Bounding boxes

kili.utils.labels.bbox

Helpers to create boundingPoly rectangle annotations.

bbox_points_to_normalized_vertices(*, bottom_left, bottom_right, top_right, top_left, img_width=None, img_height=None, origin_location='bottom_left')

Converts a bounding box defined by its 4 points to normalized vertices.

The output can be used to create a boundingPoly rectangle annotation. See the documentation for more details.

A point is a dict with keys "x" and "y", and corresponding values in pixels (int or float).

Conventions for the input points:

  • The origin is defined by the origin_location argument.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical. If origin_location is "top_left", it goes from top to bottom. If origin_location is "bottom_left", it goes from bottom to top.

Conventions for the output vertices:

  • The origin is the top left corner of the image.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical and goes from top to bottom.

If the image width and height are provided, the input point coordinates will be normalized to [0, 1]. If not, the method expects the input points' coordinates to be already normalized.

Parameters:

Name Type Description Default
bottom_left dict

Bottom left point of the bounding box.

required
bottom_right dict

Bottom right point of the bounding box.

required
top_right dict

Top right point of the bounding box.

required
top_left dict

Top left point of the bounding box.

required
img_width Union[int, float]

Width of the image the bounding box is defined in.

None
img_height Union[int, float]

Height of the image the bounding box is defined in.

None
origin_location Literal['top_left', 'bottom_left']

Location of the origin of input point coordinate system. Can be either top_left or bottom_left.

'bottom_left'

Returns:

Type Description
list

A list of normalized vertices.

Example

from kili.utils.labels.bbox import bbox_points_to_normalized_vertices

inputs = {
    bottom_left = {"x": 0, "y": 0},
    bottom_right = {"x": 10, "y": 0},
    top_right = {"x": 10, "y": 10},
    top_left = {"x": 0, "y": 10},
    img_width = 100,
    img_height = 100,
}
normalized_vertices = bbox_points_to_normalized_vertices(**inputs)
json_response = {
    "OBJECT_DETECTION_JOB": {
        "annotations": [
            {
                "boundingPoly": [{"normalizedVertices": normalized_vertices}],
                "categories": [{"name": "CLASS_A"}],
                "type": "rectangle",
            }
        ]
    }
}
Source code in kili/utils/labels/bbox.py
def bbox_points_to_normalized_vertices(
    *,
    bottom_left: dict[str, Union[int, float]],
    bottom_right: dict[str, Union[int, float]],
    top_right: dict[str, Union[int, float]],
    top_left: dict[str, Union[int, float]],
    img_width: Optional[Union[int, float]] = None,
    img_height: Optional[Union[int, float]] = None,
    origin_location: Literal["top_left", "bottom_left"] = "bottom_left",
) -> list[dict[Literal["x", "y"], float]]:
    # pylint: disable=line-too-long
    """Converts a bounding box defined by its 4 points to normalized vertices.

    The output can be used to create a boundingPoly rectangle annotation. See the [documentation](https://docs.kili-technology.com/reference/export-object-entity-detection-and-relation#standard-object-detection) for more details.

    A point is a dict with keys `"x"` and `"y"`, and corresponding values in pixels (`int` or `float`).

    Conventions for the input points:

    - The origin is defined by the `origin_location` argument.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical. If `origin_location` is `"top_left"`, it goes from top to bottom. If `origin_location` is `"bottom_left"`, it goes from bottom to top.

    Conventions for the output vertices:

    - The origin is the top left corner of the image.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical and goes from top to bottom.

    If the image width and height are provided, the input point coordinates will be normalized to `[0, 1]`.
    If not, the method expects the input points' coordinates to be already normalized.

    Args:
        bottom_left: Bottom left point of the bounding box.
        bottom_right: Bottom right point of the bounding box.
        top_right: Top right point of the bounding box.
        top_left: Top left point of the bounding box.
        img_width: Width of the image the bounding box is defined in.
        img_height: Height of the image the bounding box is defined in.
        origin_location: Location of the origin of input point coordinate system. Can be either `top_left` or `bottom_left`.

    Returns:
        A list of normalized vertices.

    !!! Example
        ```python
        from kili.utils.labels.bbox import bbox_points_to_normalized_vertices

        inputs = {
            bottom_left = {"x": 0, "y": 0},
            bottom_right = {"x": 10, "y": 0},
            top_right = {"x": 10, "y": 10},
            top_left = {"x": 0, "y": 10},
            img_width = 100,
            img_height = 100,
        }
        normalized_vertices = bbox_points_to_normalized_vertices(**inputs)
        json_response = {
            "OBJECT_DETECTION_JOB": {
                "annotations": [
                    {
                        "boundingPoly": [{"normalizedVertices": normalized_vertices}],
                        "categories": [{"name": "CLASS_A"}],
                        "type": "rectangle",
                    }
                ]
            }
        }
        ```
    """
    assert bottom_left["x"] <= bottom_right["x"], "bottom_left.x must be <= bottom_right.x"
    assert top_left["x"] <= top_right["x"], "top_left.x must be <= top_right.x"
    if origin_location == "bottom_left":
        assert bottom_left["y"] <= top_left["y"], "bottom_left.y must be <= top_left.y"
        assert bottom_right["y"] <= top_right["y"], "bottom_right.y must be <= top_right.y"
    elif origin_location == "top_left":
        assert bottom_left["y"] >= top_left["y"], "bottom_left.y must be >= top_left.y"
        assert bottom_right["y"] >= top_right["y"], "bottom_right.y must be >= top_right.y"

    if (img_width is None) != (img_height is None):
        raise ValueError("img_width and img_height must be both None or both not None.")

    return [
        point_to_normalized_point(
            point, img_width=img_width, img_height=img_height, origin_location=origin_location
        )
        for point in (bottom_left, top_left, top_right, bottom_right)
    ]

normalized_vertices_to_bbox_points(normalized_vertices, img_width=None, img_height=None, origin_location='bottom_left')

Converts a rectangle normalizedVertices annotation to a bounding box defined by 4 points.

It is the inverse of the method bbox_points_to_normalized_vertices.

A point is a dict with keys "x" and "y", and corresponding values in pixels (int or float).

Conventions for the input vertices:

  • The origin is the top left corner of the image.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical and goes from top to bottom.

Conventions for the output points (top_left, bottom_left, bottom_right, top_right):

  • The origin is defined by the origin_location argument.
  • x-axis is horizontal and goes from left to right.
  • y-axis is vertical. If origin_location is "top_left", it goes from top to bottom. If origin_location is "bottom_left", it goes from bottom to top.

If the image width and height are provided, the output point coordinates will be scaled to the image size. If not, the method will return the output points' coordinates normalized to [0, 1].

Parameters:

Name Type Description Default
normalized_vertices list

A list of normalized vertices.

required
img_width Union[int, float]

Width of the image the bounding box is defined in.

None
img_height Union[int, float]

Height of the image the bounding box is defined in.

None
origin_location Literal['top_left', 'bottom_left']

Location of the origin of output point coordinate system. Can be either top_left or bottom_left.

'bottom_left'

Returns:

Type Description
dict

A dict with keys "top_left", "bottom_left", "bottom_right", "top_right", and corresponding points.

Example

from kili.utils.labels.bbox import normalized_vertices_to_bbox_points

# if using raw dict label:
normalized_vertices = label["jsonResponse"]["OBJECT_DETECTION_JOB"]["annotations"][0]["boundingPoly"][0]["normalizedVertices"]

# if using parsed label:
normalized_vertices = label.jobs["OBJECT_DETECTION_JOB"].annotations[0].bounding_poly[0].normalized_vertices

img_height, img_width = 1080, 1920
bbox_points = normalized_vertices_to_bbox_points(normalized_vertices, img_width, img_height)
Source code in kili/utils/labels/bbox.py
def normalized_vertices_to_bbox_points(
    normalized_vertices: list[dict[str, float]],
    img_width: Optional[Union[int, float]] = None,
    img_height: Optional[Union[int, float]] = None,
    origin_location: Literal["top_left", "bottom_left"] = "bottom_left",
) -> dict[
    Literal["top_left", "bottom_left", "bottom_right", "top_right"], dict[Literal["x", "y"], float]
]:
    # pylint: disable=line-too-long
    """Converts a rectangle normalizedVertices annotation to a bounding box defined by 4 points.

    It is the inverse of the method `bbox_points_to_normalized_vertices`.

    A point is a dict with keys `"x"` and `"y"`, and corresponding values in pixels (`int` or `float`).

    Conventions for the input vertices:

    - The origin is the top left corner of the image.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical and goes from top to bottom.

    Conventions for the output points (`top_left`, `bottom_left`, `bottom_right`, `top_right`):

    - The origin is defined by the `origin_location` argument.
    - x-axis is horizontal and goes from left to right.
    - y-axis is vertical. If `origin_location` is `"top_left"`, it goes from top to bottom. If `origin_location` is `"bottom_left"`, it goes from bottom to top.

    If the image width and height are provided, the output point coordinates will be scaled to the image size.
    If not, the method will return the output points' coordinates normalized to `[0, 1]`.

    Args:
        normalized_vertices: A list of normalized vertices.
        img_width: Width of the image the bounding box is defined in.
        img_height: Height of the image the bounding box is defined in.
        origin_location: Location of the origin of output point coordinate system. Can be either `top_left` or `bottom_left`.

    Returns:
        A dict with keys `"top_left"`, `"bottom_left"`, `"bottom_right"`, `"top_right"`, and corresponding points.

    !!! Example
        ```python
        from kili.utils.labels.bbox import normalized_vertices_to_bbox_points

        # if using raw dict label:
        normalized_vertices = label["jsonResponse"]["OBJECT_DETECTION_JOB"]["annotations"][0]["boundingPoly"][0]["normalizedVertices"]

        # if using parsed label:
        normalized_vertices = label.jobs["OBJECT_DETECTION_JOB"].annotations[0].bounding_poly[0].normalized_vertices

        img_height, img_width = 1080, 1920
        bbox_points = normalized_vertices_to_bbox_points(normalized_vertices, img_width, img_height)
        ```
    """
    if len(normalized_vertices) != 4:
        raise ValueError(f"normalized_vertices must have length 4. Got {len(normalized_vertices)}.")

    if (img_width is None) != (img_height is None):
        raise ValueError("img_width and img_height must be both None or both not None.")

    img_height = img_height or 1
    img_width = img_width or 1

    ret = {}

    for vertex, point_name in zip(
        normalized_vertices, ("bottom_left", "top_left", "top_right", "bottom_right"), strict=False
    ):
        ret[point_name] = normalized_point_to_point(
            vertex, img_width=img_width, img_height=img_height, origin_location=origin_location
        )

    return ret

Polygon and segmentation masks

kili.utils.labels.image

OpenCV

It is recommended to install the image dependencies to use the image helpers.

pip install kili[image-utils]

Helpers to create boundingPoly polygon and semantic annotations.

mask_to_normalized_vertices(image)

Converts a binary mask to a list of normalized vertices using OpenCV cv2.findContours.

The output can be used to create "boundingPoly" polygon or semantic annotations. See the documentation for more details.

Parameters:

Name Type Description Default
image ndarray

Binary mask. Should be an array of shape (height, width) with values in {0, 255}.

required

Returns:

Type Description
Tuple

A tuple containing a list of normalized vertices and the hierarchy of the contours (see OpenCV documentation).

Example

import urllib.request
import cv2
from kili.utils.labels.image import mask_to_normalized_vertices

mask_url = "https://raw.githubusercontent.com/kili-technology/kili-python-sdk/main/recipes/img/HUMAN.mask.png"
urllib.request.urlretrieve(mask_url, "mask.png")

img = cv2.imread("mask.png")[:, :, 0]  # keep only height and width
img[200:220, 200:220] = 0  # add a hole in the mask to test the hierarchy

contours, hierarchy = mask_to_normalized_vertices(img)
# hierarchy tells us that the first contour is the outer contour
# and the second one is the inner contour

json_response = {
    "OBJECT_DETECTION_JOB": {
        "annotations": [
            {
                "boundingPoly": [
                    {"normalizedVertices": contours[0]},  # outer contour
                    {"normalizedVertices": contours[1]},  # inner contour
                ],
                "categories": [{"name": "A"}],
                "type": "semantic",
            }
        ]
    }
}
Source code in kili/utils/labels/image.py
def mask_to_normalized_vertices(
    image: np.ndarray,
) -> tuple[list[list[dict[str, float]]], np.ndarray]:
    # pylint: disable=line-too-long
    """Converts a binary mask to a list of normalized vertices using OpenCV [cv2.findContours](https://docs.opencv.org/4.7.0/d3/dc0/group__imgproc__shape.html#gadf1ad6a0b82947fa1fe3c3d497f260e0).

    The output can be used to create "boundingPoly" polygon or semantic annotations.
    See the [documentation](https://docs.kili-technology.com/reference/export-object-entity-detection-and-relation#standard-object-detection) for more details.

    Args:
        image: Binary mask. Should be an array of shape (height, width) with values in {0, 255}.

    Returns:
        Tuple: A tuple containing a list of normalized vertices and the hierarchy of the contours (see [OpenCV documentation](https://docs.opencv.org/4.7.0/d9/d8b/tutorial_py_contours_hierarchy.html)).

    !!! Example
        ```python
        import urllib.request
        import cv2
        from kili.utils.labels.image import mask_to_normalized_vertices

        mask_url = "https://raw.githubusercontent.com/kili-technology/kili-python-sdk/main/recipes/img/HUMAN.mask.png"
        urllib.request.urlretrieve(mask_url, "mask.png")

        img = cv2.imread("mask.png")[:, :, 0]  # keep only height and width
        img[200:220, 200:220] = 0  # add a hole in the mask to test the hierarchy

        contours, hierarchy = mask_to_normalized_vertices(img)
        # hierarchy tells us that the first contour is the outer contour
        # and the second one is the inner contour

        json_response = {
            "OBJECT_DETECTION_JOB": {
                "annotations": [
                    {
                        "boundingPoly": [
                            {"normalizedVertices": contours[0]},  # outer contour
                            {"normalizedVertices": contours[1]},  # inner contour
                        ],
                        "categories": [{"name": "A"}],
                        "type": "semantic",
                    }
                ]
            }
        }
        ```
    """
    if image.ndim > 2:
        raise ValueError(f"Image should be a 2D array, got {image.ndim}D array")

    unique_values = np.unique(image).tolist()
    if not all(value in [0, 255] for value in unique_values):
        raise ValueError(f"Image should be binary with values in {{0, 255}}, got {unique_values}")

    img_height, img_width = image.shape
    # pylint:disable=no-member
    contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  # type: ignore

    contours = [
        _opencv_contour_to_normalized_vertices(contour, img_width, img_height)
        for contour in contours
    ]
    hierarchy = hierarchy[0]

    return contours, hierarchy

normalized_vertices_to_mask(normalized_vertices, img_width, img_height)

Converts a Kili label with normalized vertices to a binary mask.

It is the inverse of the method mask_to_normalized_vertices.

Parameters:

Name Type Description Default
normalized_vertices list

A list of normalized vertices.

required
img_width int

Width of the image the segmentation is defined in.

required
img_height int

Height of the image the segmentation is defined in.

required

Returns:

Type Description
ndarray

A numpy array of shape (height, width) with values in {0, 255}.

Example

from kili.utils.labels.image import normalized_vertices_to_mask

# if using raw dict label:
normalized_vertices = label["jsonResponse"]["OBJECT_DETECTION_JOB"]["annotations"][0]["boundingPoly"][0]["normalizedVertices"]

# if using parsed label:
normalized_vertices = label.jobs["OBJECT_DETECTION_JOB"].annotations[0].bounding_poly[0].normalized_vertices

img_height, img_width = 1080, 1920
mask = normalized_vertices_to_mask(normalized_vertices, img_width, img_height)
plt.imshow(mask)
plt.show()
Source code in kili/utils/labels/image.py
def normalized_vertices_to_mask(
    normalized_vertices: list[dict[str, float]], img_width: int, img_height: int
) -> np.ndarray:
    # pylint: disable=line-too-long
    """Converts a Kili label with normalized vertices to a binary mask.

    It is the inverse of the method `mask_to_normalized_vertices`.

    Args:
        normalized_vertices: A list of normalized vertices.
        img_width: Width of the image the segmentation is defined in.
        img_height: Height of the image the segmentation is defined in.

    Returns:
        A numpy array of shape (height, width) with values in {0, 255}.

    !!! Example
        ```python
        from kili.utils.labels.image import normalized_vertices_to_mask

        # if using raw dict label:
        normalized_vertices = label["jsonResponse"]["OBJECT_DETECTION_JOB"]["annotations"][0]["boundingPoly"][0]["normalizedVertices"]

        # if using parsed label:
        normalized_vertices = label.jobs["OBJECT_DETECTION_JOB"].annotations[0].bounding_poly[0].normalized_vertices

        img_height, img_width = 1080, 1920
        mask = normalized_vertices_to_mask(normalized_vertices, img_width, img_height)
        plt.imshow(mask)
        plt.show()
        ```
    """
    mask = np.zeros((img_height, img_width), dtype=np.uint8)
    polygon = [
        [
            int(round(vertice["x"] * img_width)),
            int(round(vertice["y"] * img_height)),
        ]
        for vertice in normalized_vertices
    ]
    polygon = np.array([polygon])
    cv2.fillPoly(img=mask, pts=polygon, color=255)  # type: ignore  # pylint:disable=no-member
    return mask

GeoJson

Info

Label coordinates of GeoTIFF files (with geospatial metadata) are expressed in latitude and longitude where x stands for longitude and y for latitude.

Read more about Kili labeling features for geospatial imagery here.

Warning

If the geotiff image asset does not have geospatial metadata, the coordinates will be expressed in normalized coordinates, and the export to GeoJSON will not be accurate since the geospatial information is missing.

To check if your image asset has geospatial metadata, you can use the following code snippet:

>>> asset = kili.assets(..., fields=["jsonContent"])[0]
>>> print(asset['jsonContent'])

# asset without geospatial metadata
[{"imageUrl": "https://...", "initEpsg": -1, "useClassicCoordinates": true}]

# asset with geospatial metadata
# note that the epsg and initEpsg may be different for your asset
[{"bounds": [[...], [...]], "epsg": "EPSG4326", "imageUrl": "https://...", "initEpsg": 4326, "useClassicCoordinates": false}]

Point

Point label utils.

geojson_point_feature_to_kili_point_annotation(point, categories=None, children=None, mid=None)

Convert a geojson point feature to a Kili point annotation.

Parameters:

Name Type Description Default
point Dict[str, Any]

a geojson point feature.

required
categories Optional[List[Dict]]

the categories of the annotation. If not provided, the categories are taken from the kili key of the geojson feature properties.

None
children Optional[Dict]

the children of the annotation. If not provided, the children are taken from the kili key of the geojson feature properties.

None
mid Optional[str]

the mid of the annotation. If not provided, the mid is taken from the id key of the geojson feature.

None

Returns:

Type Description
Dict[str, Any]

A Kili point annotation.

Example

>>> point = {
    'type': 'Feature',
    'geometry': {'type': 'Point', 'coordinates': [-79.0, -3.0]},
    'id': 'mid_object',
    'properties': {'kili': {'categories': [{'name': 'A'}]}}
}
>>> geojson_point_feature_to_kili_point_annotation(point)
{
    'children': {},
    'point': {'x': -79.0, 'y': -3.0},
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'marker'
}
Source code in kili_formats/format/geojson/point.py
def geojson_point_feature_to_kili_point_annotation(
    point: Dict[str, Any],
    categories: Optional[List[Dict]] = None,
    children: Optional[Dict] = None,
    mid: Optional[str] = None,
) -> Dict[str, Any]:
    """Convert a geojson point feature to a Kili point annotation.

    Args:
        point: a geojson point feature.
        categories: the categories of the annotation.
            If not provided, the categories are taken from the `kili` key of the geojson feature properties.
        children: the children of the annotation.
            If not provided, the children are taken from the `kili` key of the geojson feature properties.
        mid: the mid of the annotation.
            If not provided, the mid is taken from the `id` key of the geojson feature.

    Returns:
        A Kili point annotation.

    !!! Example
        ```python
        >>> point = {
            'type': 'Feature',
            'geometry': {'type': 'Point', 'coordinates': [-79.0, -3.0]},
            'id': 'mid_object',
            'properties': {'kili': {'categories': [{'name': 'A'}]}}
        }
        >>> geojson_point_feature_to_kili_point_annotation(point)
        {
            'children': {},
            'point': {'x': -79.0, 'y': -3.0},
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'marker'
        }
        ```
    """
    assert point.get("type") == "Feature", f"Feature type must be `Feature`, got: {point['type']}"
    assert (
        point["geometry"]["type"] == "Point"
    ), f"Geometry type must be `Point`, got: {point['geometry']['type']}"

    children = children or point["properties"].get("kili", {}).get("children", {})
    categories = categories or point["properties"]["kili"]["categories"]

    ret = {
        "children": children,
        "categories": categories,
        "type": "marker",
    }
    ret["point"] = {
        "x": point["geometry"]["coordinates"][0],
        "y": point["geometry"]["coordinates"][1],
    }

    if mid is not None:
        ret["mid"] = str(mid)
    elif "id" in point:
        ret["mid"] = str(point["id"])

    return ret

kili_point_annotation_to_geojson_point_feature(point_annotation, job_name=None)

Convert a Kili point annotation to a geojson point feature.

Parameters:

Name Type Description Default
point_annotation Dict[str, Any]

a Kili point annotation.

required
job_name Optional[str]

the name of the job to which the annotation belongs.

None

Returns:

Type Description
Dict[str, Any]

A geojson point feature.

Example

>>> point = {
    'children': {},
    'point': {'x': -79.0, 'y': -3.0},
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'marker'
}
>>> kili_point_annotation_to_geojson_point_feature(point)
{
    'type': 'Feature',
    'geometry': {
        'type': 'Point',
        'coordinates': [-79.0, -3.0]},
        'id': 'mid_object',
        'properties': {
            'kili': {
                'categories': [{'name': 'A'}],
                'children': {},
                'type': 'marker'
            }
        }
    }
}
Source code in kili_formats/format/geojson/point.py
def kili_point_annotation_to_geojson_point_feature(
    point_annotation: Dict[str, Any], job_name: Optional[str] = None
) -> Dict[str, Any]:
    """Convert a Kili point annotation to a geojson point feature.

    Args:
        point_annotation: a Kili point annotation.
        job_name: the name of the job to which the annotation belongs.

    Returns:
        A geojson point feature.

    !!! Example
        ```python
        >>> point = {
            'children': {},
            'point': {'x': -79.0, 'y': -3.0},
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'marker'
        }
        >>> kili_point_annotation_to_geojson_point_feature(point)
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': [-79.0, -3.0]},
                'id': 'mid_object',
                'properties': {
                    'kili': {
                        'categories': [{'name': 'A'}],
                        'children': {},
                        'type': 'marker'
                    }
                }
            }
        }
        ```
    """
    point = point_annotation
    assert point["type"] == "marker", f"Annotation type must be `marker`, got: {point['type']}"

    ret = {"type": "Feature", "geometry": kili_point_to_geojson_point(point["point"])}
    if "mid" in point:
        ret["id"] = point["mid"]
    ret["properties"] = {"kili": {k: v for k, v in point.items() if k not in ["point", "mid"]}}
    if job_name is not None:
        ret["properties"]["kili"]["job"] = job_name
    return ret

kili_point_to_geojson_point(point)

Convert a Kili point to a geojson point.

Parameters:

Name Type Description Default
point Dict[str, float]

a Kili point (vertex).

required

Returns:

Type Description
Dict[str, Any]

A geojson point.

Example

>>> point = {"x": 1.0, "y": 2.0}
>>> kili_point_to_geojson_point(point)
{
    "type": "Point",
    "coordinates": [1.0, 2.0]
}
Source code in kili_formats/format/geojson/point.py
def kili_point_to_geojson_point(point: Dict[str, float]) -> Dict[str, Any]:
    """Convert a Kili point to a geojson point.

    Args:
        point: a Kili point (vertex).

    Returns:
        A geojson point.

    !!! Example
        ```python
        >>> point = {"x": 1.0, "y": 2.0}
        >>> kili_point_to_geojson_point(point)
        {
            "type": "Point",
            "coordinates": [1.0, 2.0]
        }
        ```
    """
    return {"type": "Point", "coordinates": [point["x"], point["y"]]}

Line

Geojson linestring utilities.

geojson_linestring_feature_to_kili_line_annotation(line, categories=None, children=None, mid=None)

Convert a geojson linestring feature to a Kili line annotation.

Parameters:

Name Type Description Default
line Dict[str, Any]

a geojson linestring feature.

required
categories Optional[List[Dict]]

the categories of the annotation. If not provided, the categories are taken from the kili key of the geojson feature properties.

None
children Optional[Dict]

the children of the annotation. If not provided, the children are taken from the kili key of the geojson feature properties.

None
mid Optional[str]

the mid of the annotation. If not provided, the mid is taken from the id key of the geojson feature.

None

Returns:

Type Description
Dict[str, Any]

A Kili line annotation.

Example

>>> line = {
    'type': 'Feature',
    'geometry': {
        'type': 'LineString',
        'coordinates': [[-79.0, -3.0], [-79.0, -3.0]]},
    }
    'id': 'mid_object',
    'properties': {
        'kili': {
            'categories': [{'name': 'A'}],
            'children': {},
            'job': 'job_name'
        }
    }
}
>>> geojson_linestring_feature_to_kili_line_annotation(line)
{
    'children': {},
    'polyline': [{'x': -79.0, 'y': -3.0}, {'x': -79.0, 'y': -3.0}],
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'polyline'
}
Source code in kili_formats/format/geojson/line.py
def geojson_linestring_feature_to_kili_line_annotation(
    line: Dict[str, Any],
    categories: Optional[List[Dict]] = None,
    children: Optional[Dict] = None,
    mid: Optional[str] = None,
) -> Dict[str, Any]:
    """Convert a geojson linestring feature to a Kili line annotation.

    Args:
        line: a geojson linestring feature.
        categories: the categories of the annotation.
            If not provided, the categories are taken from the `kili` key of the geojson feature properties.
        children: the children of the annotation.
            If not provided, the children are taken from the `kili` key of the geojson feature properties.
        mid: the mid of the annotation.
            If not provided, the mid is taken from the `id` key of the geojson feature.

    Returns:
        A Kili line annotation.

    !!! Example
        ```python
        >>> line = {
            'type': 'Feature',
            'geometry': {
                'type': 'LineString',
                'coordinates': [[-79.0, -3.0], [-79.0, -3.0]]},
            }
            'id': 'mid_object',
            'properties': {
                'kili': {
                    'categories': [{'name': 'A'}],
                    'children': {},
                    'job': 'job_name'
                }
            }
        }
        >>> geojson_linestring_feature_to_kili_line_annotation(line)
        {
            'children': {},
            'polyline': [{'x': -79.0, 'y': -3.0}, {'x': -79.0, 'y': -3.0}],
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'polyline'
        }
        ```
    """
    assert line["type"] == "Feature", f"Feature type must be `Feature`, got: {line['type']}"
    assert (
        line["geometry"]["type"] == "LineString"
    ), f"Geometry type must be `LineString`, got: {line['geometry']['type']}"

    children = children or line["properties"].get("kili", {}).get("children", {})
    categories = categories or line["properties"]["kili"]["categories"]

    ret = {
        "children": children,
        "categories": categories,
        "type": "polyline",
    }
    ret["polyline"] = [{"x": coord[0], "y": coord[1]} for coord in line["geometry"]["coordinates"]]

    if mid is not None:
        ret["mid"] = str(mid)
    elif "id" in line:
        ret["mid"] = str(line["id"])

    return ret

kili_line_annotation_to_geojson_linestring_feature(polyline_annotation, job_name=None)

Convert a Kili line annotation to a geojson linestring feature.

Parameters:

Name Type Description Default
polyline_annotation Dict[str, Any]

a Kili line annotation.

required
job_name Optional[str]

the name of the job to which the annotation belongs.

None

Returns:

Type Description
Dict[str, Any]

A geojson linestring feature.

Example

>>> polyline = {
    'children': {},
    'polyline': [{'x': -79.0, 'y': -3.0}, {'x': -79.0, 'y': -3.0}],
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'polyline'
}
>>> kili_line_annotation_to_geojson_linestring_feature(polyline, 'job_name')
{
    'type': 'Feature',
    'geometry': {
        'type': 'LineString',
        'coordinates': [[-79.0, -3.0], [-79.0, -3.0]]},
        'id': 'mid_object',
        'properties': {
            'kili': {
                'categories': [{'name': 'A'}],
                'children': {},
                'job': 'job_name'
            }
        }
}
Source code in kili_formats/format/geojson/line.py
def kili_line_annotation_to_geojson_linestring_feature(
    polyline_annotation: Dict[str, Any], job_name: Optional[str] = None
) -> Dict[str, Any]:
    """Convert a Kili line annotation to a geojson linestring feature.

    Args:
        polyline_annotation: a Kili line annotation.
        job_name: the name of the job to which the annotation belongs.

    Returns:
        A geojson linestring feature.

    !!! Example
        ```python
        >>> polyline = {
            'children': {},
            'polyline': [{'x': -79.0, 'y': -3.0}, {'x': -79.0, 'y': -3.0}],
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'polyline'
        }
        >>> kili_line_annotation_to_geojson_linestring_feature(polyline, 'job_name')
        {
            'type': 'Feature',
            'geometry': {
                'type': 'LineString',
                'coordinates': [[-79.0, -3.0], [-79.0, -3.0]]},
                'id': 'mid_object',
                'properties': {
                    'kili': {
                        'categories': [{'name': 'A'}],
                        'children': {},
                        'job': 'job_name'
                    }
                }
        }
        ```
    """
    assert (
        polyline_annotation["type"] == "polyline"
    ), f"Annotation type must be `polyline`, got: {polyline_annotation['type']}"

    ret = {
        "type": "Feature",
        "geometry": kili_line_to_geojson_linestring(polyline_annotation["polyline"]),
    }
    if "mid" in polyline_annotation:
        ret["id"] = polyline_annotation["mid"]
    ret["properties"] = {
        "kili": {k: v for k, v in polyline_annotation.items() if k not in ["mid", "polyline"]}
    }
    if job_name is not None:
        ret["properties"]["kili"]["job"] = job_name
    return ret

kili_line_to_geojson_linestring(polyline)

Convert a Kili line to a geojson linestring.

Parameters:

Name Type Description Default
polyline List[Dict[str, float]]

a Kili line (polyline).

required

Returns:

Type Description
Dict[str, Any]

A geojson linestring.

Example

>>> polyline = [{"x": 1.0, "y": 2.0}, {"x": 3.0, "y": 4.0}]
>>> kili_line_to_geojson_linestring(polyline)
{
    "type": "LineString",
    "coordinates": [[1.0, 2.0], [3.0, 4.0]]
}
Source code in kili_formats/format/geojson/line.py
def kili_line_to_geojson_linestring(polyline: List[Dict[str, float]]) -> Dict[str, Any]:
    """Convert a Kili line to a geojson linestring.

    Args:
        polyline: a Kili line (polyline).

    Returns:
        A geojson linestring.

    !!! Example
        ```python
        >>> polyline = [{"x": 1.0, "y": 2.0}, {"x": 3.0, "y": 4.0}]
        >>> kili_line_to_geojson_linestring(polyline)
        {
            "type": "LineString",
            "coordinates": [[1.0, 2.0], [3.0, 4.0]]
        }
        ```
    """
    ret = {"type": "LineString", "coordinates": []}
    ret["coordinates"] = [[vertex["x"], vertex["y"]] for vertex in polyline]
    return ret  # type: ignore

Bounding box

Bounding box conversion functions between Kili and geojson formats.

geojson_polygon_feature_to_kili_bbox_annotation(polygon, categories=None, children=None, mid=None)

Convert a geojson polygon feature to a Kili bounding box annotation.

Parameters:

Name Type Description Default
polygon Dict[str, Any]

a geojson polygon feature.

required
categories Optional[List[Dict]]

the categories of the annotation. If not provided, the categories are taken from the kili key of the geojson feature properties.

None
children Optional[Dict]

the children of the annotation. If not provided, the children are taken from the kili key of the geojson feature properties.

None
mid Optional[str]

the mid of the annotation. If not provided, the mid is taken from the id key of the geojson feature.

None

Returns:

Type Description
Dict[str, Any]

A Kili bounding box annotation.

Example

>>> polygon = {
    'type': 'Feature',
    'geometry': {
        'type': 'Polygon',
        'coordinates': [
            [
                [-12.6, 12.87],
                [-42.6, 22.17],
                [-17.6, -22.4],
                [2.6, -1.87],
                [-12.6, 12.87]
            ]
        ]
    },
    'id': 'mid_object',
    'properties': {
        'kili': {
            'categories': [{'name': 'A'}],
            'children': {},
            'type': 'rectangle',
            'job': 'job_name'
        }
    }
}
>>> geojson_polygon_feature_to_kili_bbox_annotation(polygon)
{
    'children': {},
    'boundingPoly': [
            {
                'normalizedVertices': [
                    {'x': -12.6, 'y': 12.87},
                    {'x': -42.6, 'y': 22.17},
                    {'x': -17.6, 'y': -22.4},
                    {'x': 2.6, 'y': -1.87}
                ]
            }
        ],
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'rectangle'
}
Source code in kili_formats/format/geojson/bbox.py
def geojson_polygon_feature_to_kili_bbox_annotation(
    polygon: Dict[str, Any],
    categories: Optional[List[Dict]] = None,
    children: Optional[Dict] = None,
    mid: Optional[str] = None,
) -> Dict[str, Any]:
    """Convert a geojson polygon feature to a Kili bounding box annotation.

    Args:
        polygon: a geojson polygon feature.
        categories: the categories of the annotation.
            If not provided, the categories are taken from the `kili` key of the geojson feature properties.
        children: the children of the annotation.
            If not provided, the children are taken from the `kili` key of the geojson feature properties.
        mid: the mid of the annotation.
            If not provided, the mid is taken from the `id` key of the geojson feature.

    Returns:
        A Kili bounding box annotation.

    !!! Example
        ```python
        >>> polygon = {
            'type': 'Feature',
            'geometry': {
                'type': 'Polygon',
                'coordinates': [
                    [
                        [-12.6, 12.87],
                        [-42.6, 22.17],
                        [-17.6, -22.4],
                        [2.6, -1.87],
                        [-12.6, 12.87]
                    ]
                ]
            },
            'id': 'mid_object',
            'properties': {
                'kili': {
                    'categories': [{'name': 'A'}],
                    'children': {},
                    'type': 'rectangle',
                    'job': 'job_name'
                }
            }
        }
        >>> geojson_polygon_feature_to_kili_bbox_annotation(polygon)
        {
            'children': {},
            'boundingPoly': [
                    {
                        'normalizedVertices': [
                            {'x': -12.6, 'y': 12.87},
                            {'x': -42.6, 'y': 22.17},
                            {'x': -17.6, 'y': -22.4},
                            {'x': 2.6, 'y': -1.87}
                        ]
                    }
                ],
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'rectangle'
        }
        ```
    """
    assert (
        polygon.get("type") == "Feature"
    ), f"Feature type must be `Feature`, got: {polygon['type']}"
    assert (
        polygon["geometry"]["type"] == "Polygon"
    ), f"Geometry type must be `Polygon`, got: {polygon['geometry']['type']}"

    children = children or polygon["properties"].get("kili", {}).get("children", {})
    categories = categories or polygon["properties"]["kili"]["categories"]

    ret = {
        "children": children,
        "categories": categories,
        "type": "rectangle",
    }
    # geojson polygon has one more point than kili bounding box
    coords = polygon["geometry"]["coordinates"][0]
    normalized_vertices = [
        {"x": coords[0][0], "y": coords[0][1]},
        {"x": coords[3][0], "y": coords[3][1]},
        {"x": coords[2][0], "y": coords[2][1]},
        {"x": coords[1][0], "y": coords[1][1]},
    ]
    ret["boundingPoly"] = [{"normalizedVertices": normalized_vertices}]

    if mid is not None:
        ret["mid"] = mid
    elif "id" in polygon:
        ret["mid"] = polygon["id"]

    return ret

kili_bbox_annotation_to_geojson_polygon_feature(bbox_annotation, job_name=None)

Convert a Kili bounding box annotation to a geojson polygon feature.

Parameters:

Name Type Description Default
bbox_annotation Dict[str, Any]

a Kili bounding box annotation.

required
job_name Optional[str]

the name of the job to which the annotation belongs.

None

Returns:

Type Description
Dict[str, Any]

A geojson polygon feature.

Example

>>> bbox = {
    'children': {},
    'boundingPoly': [
            {
                'normalizedVertices': [
                    {'x': -12.6, 'y': 12.87},
                    {'x': -42.6, 'y': 22.17},
                    {'x': -17.6, 'y': -22.4},
                    {'x': 2.6, 'y': -1.87}
                ]
            }
        ],
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'rectangle'
}
>>> kili_bbox_annotation_to_geojson_polygon_feature(bbox, 'job_name')
{
    'type': 'Feature',
    'geometry': {
        'type': 'Polygon',
        'coordinates': [
            [
                [-12.6, 12.87],
                [-42.6, 22.17],
                [-17.6, -22.4],
                [2.6, -1.87],
                [-12.6, 12.87]
            ]
        ]
    },
    'id': 'mid_object',
    'properties': {
        'kili': {
            'categories': [{'name': 'A'}],
            'children': {},
            'type': 'rectangle',
            'job': 'job_name'
        }
    }
}
Source code in kili_formats/format/geojson/bbox.py
def kili_bbox_annotation_to_geojson_polygon_feature(
    bbox_annotation: Dict[str, Any], job_name: Optional[str] = None
) -> Dict[str, Any]:
    """Convert a Kili bounding box annotation to a geojson polygon feature.

    Args:
        bbox_annotation: a Kili bounding box annotation.
        job_name: the name of the job to which the annotation belongs.

    Returns:
        A geojson polygon feature.

    !!! Example
        ```python
        >>> bbox = {
            'children': {},
            'boundingPoly': [
                    {
                        'normalizedVertices': [
                            {'x': -12.6, 'y': 12.87},
                            {'x': -42.6, 'y': 22.17},
                            {'x': -17.6, 'y': -22.4},
                            {'x': 2.6, 'y': -1.87}
                        ]
                    }
                ],
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'rectangle'
        }
        >>> kili_bbox_annotation_to_geojson_polygon_feature(bbox, 'job_name')
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Polygon',
                'coordinates': [
                    [
                        [-12.6, 12.87],
                        [-42.6, 22.17],
                        [-17.6, -22.4],
                        [2.6, -1.87],
                        [-12.6, 12.87]
                    ]
                ]
            },
            'id': 'mid_object',
            'properties': {
                'kili': {
                    'categories': [{'name': 'A'}],
                    'children': {},
                    'type': 'rectangle',
                    'job': 'job_name'
                }
            }
        }
        ```
    """
    bbox = bbox_annotation
    assert bbox["type"] == "rectangle", f"Annotation type must be `rectangle`, got: {bbox['type']}"

    ret = {
        "type": "Feature",
        "geometry": kili_bbox_to_geojson_polygon(bbox["boundingPoly"][0]["normalizedVertices"]),
    }
    if "mid" in bbox:
        ret["id"] = bbox["mid"]
    ret["properties"] = {
        "kili": {k: v for k, v in bbox.items() if k not in ["boundingPoly", "mid"]}
    }
    if job_name is not None:
        ret["properties"]["kili"]["job"] = job_name
    return ret

kili_bbox_to_geojson_polygon(vertices)

Convert a Kili bounding box to a geojson polygon.

Parameters:

Name Type Description Default
vertices List[Dict[str, float]]

Kili bounding polygon vertices.

required

Returns:

Type Description
Dict[str, Any]

A geojson polygon.

Example

>>> vertices = [
    {'x': 12.0, 'y': 3.0},
    {'x': 12.0, 'y': 4.0},
    {'x': 13.0, 'y': 4.0},
    {'x': 13.0, 'y': 3.0}
]
>>> kili_bbox_to_geojson_polygon(vertices)
{
    'type': 'Polygon',
    'coordinates': [
        [
            [12.0, 3.0],
            [12.0, 4.0],
            [13.0, 4.0],
            [13.0, 3.0],
            [12.0, 3.0]
        ]
    ]
}
Source code in kili_formats/format/geojson/bbox.py
def kili_bbox_to_geojson_polygon(vertices: List[Dict[str, float]]) -> Dict[str, Any]:
    """Convert a Kili bounding box to a geojson polygon.

    Args:
        vertices: Kili bounding polygon vertices.

    Returns:
        A geojson polygon.

    !!! Example
        ```python
        >>> vertices = [
            {'x': 12.0, 'y': 3.0},
            {'x': 12.0, 'y': 4.0},
            {'x': 13.0, 'y': 4.0},
            {'x': 13.0, 'y': 3.0}
        ]
        >>> kili_bbox_to_geojson_polygon(vertices)
        {
            'type': 'Polygon',
            'coordinates': [
                [
                    [12.0, 3.0],
                    [12.0, 4.0],
                    [13.0, 4.0],
                    [13.0, 3.0],
                    [12.0, 3.0]
                ]
            ]
        }
        ```
    """
    vertex_name_to_value = {}
    for vertex, point_name in zip(
        vertices, ("bottom_left", "top_left", "top_right", "bottom_right")
    ):
        vertex_name_to_value[point_name] = vertex

    ret = {"type": "Polygon", "coordinates": []}
    ret["coordinates"] = [
        [
            [vertex_name_to_value["bottom_left"]["x"], vertex_name_to_value["bottom_left"]["y"]],
            [vertex_name_to_value["bottom_right"]["x"], vertex_name_to_value["bottom_right"]["y"]],
            [vertex_name_to_value["top_right"]["x"], vertex_name_to_value["top_right"]["y"]],
            [vertex_name_to_value["top_left"]["x"], vertex_name_to_value["top_left"]["y"]],
            [vertex_name_to_value["bottom_left"]["x"], vertex_name_to_value["bottom_left"]["y"]],
        ]
    ]

    return ret

Polygon

Polygon label utils.

geojson_polygon_feature_to_kili_polygon_annotation(polygon, categories=None, children=None, mid=None)

Convert a geojson polygon feature to a Kili polygon annotation.

Parameters:

Name Type Description Default
polygon Dict[str, Any]

a geojson polygon feature.

required
categories Optional[List[Dict]]

the categories of the annotation. If not provided, the categories are taken from the kili key of the geojson feature properties.

None
children Optional[Dict]

the children of the annotation. If not provided, the children are taken from the kili key of the geojson feature properties.

None
mid Optional[str]

the mid of the annotation. If not provided, the mid is taken from the id key of the geojson feature.

None

Returns:

Type Description
Dict[str, Any]

A Kili polygon annotation.

Example

>>> polygon = {
    'type': 'Feature',
    'geometry': {
        'type': 'Polygon',
        'coordinates': [[[-79.0, -3.0], [-79.0, -3.0]]]},
    },
    'id': 'mid_object',
    'properties': {
        'kili': {
            'categories': [{'name': 'A'}],
            'children': {},
            'type': 'polygon',
            'job': 'job_name'
        }
    }
}
>>> geojson_polygon_feature_to_kili_polygon_annotation(polygon)
{
    'children': {},
    'boundingPoly': [{'normalizedVertices': [{'x': -79.0, 'y': -3.0}]}],
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'polygon'
}
Source code in kili_formats/format/geojson/polygon.py
def geojson_polygon_feature_to_kili_polygon_annotation(
    polygon: Dict[str, Any],
    categories: Optional[List[Dict]] = None,
    children: Optional[Dict] = None,
    mid: Optional[str] = None,
) -> Dict[str, Any]:
    """Convert a geojson polygon feature to a Kili polygon annotation.

    Args:
        polygon: a geojson polygon feature.
        categories: the categories of the annotation.
            If not provided, the categories are taken from the `kili` key of the geojson feature properties.
        children: the children of the annotation.
            If not provided, the children are taken from the `kili` key of the geojson feature properties.
        mid: the mid of the annotation.
            If not provided, the mid is taken from the `id` key of the geojson feature.


    Returns:
        A Kili polygon annotation.

    !!! Example
        ```python
        >>> polygon = {
            'type': 'Feature',
            'geometry': {
                'type': 'Polygon',
                'coordinates': [[[-79.0, -3.0], [-79.0, -3.0]]]},
            },
            'id': 'mid_object',
            'properties': {
                'kili': {
                    'categories': [{'name': 'A'}],
                    'children': {},
                    'type': 'polygon',
                    'job': 'job_name'
                }
            }
        }
        >>> geojson_polygon_feature_to_kili_polygon_annotation(polygon)
        {
            'children': {},
            'boundingPoly': [{'normalizedVertices': [{'x': -79.0, 'y': -3.0}]}],
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'polygon'
        }
        ```
    """
    assert (
        polygon.get("type") == "Feature"
    ), f"Feature type must be `Feature`, got: {polygon['type']}"
    assert (
        polygon["geometry"]["type"] == "Polygon"
    ), f"Geometry type must be `Polygon`, got: {polygon['geometry']['type']}"

    children = children or polygon["properties"].get("kili", {}).get("children", {})
    categories = categories or polygon["properties"]["kili"]["categories"]

    ret = {
        "children": children,
        "categories": categories,
        "type": "polygon",
    }
    coords = polygon["geometry"]["coordinates"][0]
    normalized_vertices = [{"x": coord[0], "y": coord[1]} for coord in coords[:-1]]
    ret["boundingPoly"] = [{"normalizedVertices": normalized_vertices}]

    if mid is not None:
        ret["mid"] = str(mid)
    elif "id" in polygon:
        ret["mid"] = str(polygon["id"])

    return ret

get_oriented_area(vertices)

Returns the area value which gives an indication on the vertices order. Positive if counter-clockwise, negative if clockwise.

This function uses the Shoelace formula : see also : https://en.wikipedia.org/wiki/Shoelace_formula

Source code in kili_formats/format/geojson/polygon.py
def get_oriented_area(vertices):
    """Returns the area value which gives an indication on the vertices order.
    Positive if counter-clockwise, negative if clockwise.

    This function uses the Shoelace formula :
    see also : https://en.wikipedia.org/wiki/Shoelace_formula
    """
    n = len(vertices)
    sum_product = 0

    for i in range(n):
        x1, y1 = vertices[i]["x"], vertices[i]["y"]
        x2, y2 = vertices[(i + 1) % n]["x"], vertices[(i + 1) % n]["y"]
        sum_product += (x1 - x2) * (y2 + y1)

    return sum_product

kili_polygon_annotation_to_geojson_polygon_feature(polygon_annotation, job_name=None)

Convert a Kili polygon annotation to a geojson polygon feature.

Parameters:

Name Type Description Default
polygon_annotation Dict[str, Any]

a Kili polygon annotation.

required
job_name Optional[str]

the name of the job to which the annotation belongs.

None

Returns:

Type Description
Dict[str, Any]

A geojson polygon feature.

Example

>>> polygon = {
    'children': {},
    'boundingPoly': [{'normalizedVertices': [{'x': -79.0, 'y': -3.0}]}],
    'categories': [{'name': 'A'}],
    'mid': 'mid_object',
    'type': 'polygon'
}
>>> kili_polygon_annotation_to_geojson_polygon_feature(polygon, 'job_name')
{
    'type': 'Feature',
    'geometry': {
        'type': 'Polygon',
        'coordinates': [[[-79.0, -3.0], [-79.0, -3.0]]]},
        'id': 'mid_object',
        'properties': {
            'kili': {
                'categories': [{'name': 'A'}],
                'children': {},
                'type': 'polygon',
                'job': 'job_name'
            }
        }
    }
}
Source code in kili_formats/format/geojson/polygon.py
def kili_polygon_annotation_to_geojson_polygon_feature(
    polygon_annotation: Dict[str, Any], job_name: Optional[str] = None
) -> Dict[str, Any]:
    """Convert a Kili polygon annotation to a geojson polygon feature.

    Args:
        polygon_annotation: a Kili polygon annotation.
        job_name: the name of the job to which the annotation belongs.

    Returns:
        A geojson polygon feature.

    !!! Example
        ```python
        >>> polygon = {
            'children': {},
            'boundingPoly': [{'normalizedVertices': [{'x': -79.0, 'y': -3.0}]}],
            'categories': [{'name': 'A'}],
            'mid': 'mid_object',
            'type': 'polygon'
        }
        >>> kili_polygon_annotation_to_geojson_polygon_feature(polygon, 'job_name')
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Polygon',
                'coordinates': [[[-79.0, -3.0], [-79.0, -3.0]]]},
                'id': 'mid_object',
                'properties': {
                    'kili': {
                        'categories': [{'name': 'A'}],
                        'children': {},
                        'type': 'polygon',
                        'job': 'job_name'
                    }
                }
            }
        }
        ```
    """
    polygon = polygon_annotation
    assert (
        polygon["type"] == "polygon"
    ), f"Annotation type must be `polygon`, got: {polygon['type']}"

    ret = {
        "type": "Feature",
        "geometry": kili_polygon_to_geojson_polygon(
            polygon["boundingPoly"][0]["normalizedVertices"]
        ),
    }
    if "mid" in polygon:
        ret["id"] = polygon["mid"]
    ret["properties"] = {
        "kili": {k: v for k, v in polygon.items() if k not in ["boundingPoly", "mid"]}
    }
    if job_name is not None:
        ret["properties"]["kili"]["job"] = job_name
    return ret

kili_polygon_to_geojson_polygon(vertices)

Convert a Kili polygon to a geojson polygon.

Parameters:

Name Type Description Default
vertices List[Dict[str, float]]

Kili polygon vertices.

required

Returns:

Type Description
Dict[str, Any]

A geojson polygon.

Example

>>> vertices = [
    {'x': 10.42, 'y': 27.12},
    {'x': 1.53, 'y': 14.57},
    {'x': 147.45, 'y': 14.12},
    {'x': 14.23, 'y': 0.23}
]
>>> kili_polygon_to_geojson_polygon(vertices)
{
    'type': 'Polygon',
    'coordinates': [
        [
            [10.42, 27.12],
            [1.53, 14.57],
            [147.45, 14.12],
            [14.23, 0.23],
            [10.42, 27.12]
        ]
    ]
}
Source code in kili_formats/format/geojson/polygon.py
def kili_polygon_to_geojson_polygon(vertices: List[Dict[str, float]]) -> Dict[str, Any]:
    """Convert a Kili polygon to a geojson polygon.

    Args:
        vertices: Kili polygon vertices.

    Returns:
        A geojson polygon.

    !!! Example
        ```python
        >>> vertices = [
            {'x': 10.42, 'y': 27.12},
            {'x': 1.53, 'y': 14.57},
            {'x': 147.45, 'y': 14.12},
            {'x': 14.23, 'y': 0.23}
        ]
        >>> kili_polygon_to_geojson_polygon(vertices)
        {
            'type': 'Polygon',
            'coordinates': [
                [
                    [10.42, 27.12],
                    [1.53, 14.57],
                    [147.45, 14.12],
                    [14.23, 0.23],
                    [10.42, 27.12]
                ]
            ]
        }
        ```
    """
    reordered_polygon_vertices = order_counter_clockwise(vertices)
    polygon = [[vertex["x"], vertex["y"]] for vertex in reordered_polygon_vertices]

    polygon.append(polygon[0])  # the first and last positions must be the same
    return {"type": "Polygon", "coordinates": [polygon]}

order_counter_clockwise(vertices)

Returns the vertices, in the correct order :

If the vertices are set clockwise, we reverse them to have them in the anti-clockwise order. For more information on the order expected for GeoJson : https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6

Source code in kili_formats/format/geojson/polygon.py
def order_counter_clockwise(vertices):
    """Returns the vertices, in the correct order :

    If the vertices are set clockwise, we reverse them to have them in the anti-clockwise order.
    For more information on the order expected for GeoJson :
    https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6
    """
    order = get_oriented_area(vertices)
    if order < 0:
        vertices.reverse()
    elif order == 0:
        raise ConversionError(
            f"Polygon order could not be identified as clockwise nor counter-clockwise because of \
                edges intersection in {vertices} and thus cannot be exported to GeoJson format."
        )

    return vertices

Segmentation

Geojson segmentation utilities.

_is_hierarchical_format(bounding_poly) private

Check if boundingPoly is in hierarchical format.

Hierarchical: [ [ {normalizedVertices: [...]}, ... ], ... ] Flat: [ {normalizedVertices: [...]}, ... ]

Source code in kili_formats/format/geojson/segmentation.py
def _is_hierarchical_format(bounding_poly):
    """Check if boundingPoly is in hierarchical format.

    Hierarchical: [ [ {normalizedVertices: [...]}, ... ], ... ]
    Flat: [ {normalizedVertices: [...]}, ... ]
    """
    if not bounding_poly or len(bounding_poly) == 0:
        return False

    first_element = bounding_poly[0]

    if isinstance(first_element, list):
        return True

    if isinstance(first_element, dict) and "normalizedVertices" in first_element:
        return False

    return False

geojson_polygon_feature_to_kili_segmentation_annotation(polygon, categories=None, children=None, mid=None)

Convert a geojson polygon feature to a list of Kili segmentation annotations.

Parameters:

Name Type Description Default
polygon Dict[str, Any]

A geojson polygon feature.

required
categories Optional[List[Dict]]

The categories of the annotation. If not provided, the categories are taken from the kili key of the geojson feature properties.

None
children Optional[Dict]

The children of the annotation. If not provided, the children are taken from the kili key of the geojson feature properties.

None
mid Optional[str]

The mid of the annotation. If not provided, the mid is taken from the id key of the geojson feature. If no id is available, a new UUID is generated.

None

Returns:

Type Description
List[Dict[str, Any]]

A list of Kili segmentation annotations. Each annotation has boundingPoly of dimension 2.

The first dimensions corresponds to the polygon parts (1 for Polygon, N for MultiPolygon). The second dimension corresponds to the rings of each polygon part (exterior + holes).

Example

# Polygon feature
>>> polygon = {
...     'type': 'Feature',
...     'geometry': {
...         'type': 'Polygon',
...         'coordinates': [
...             [[0, 0], [1, 0], [1, 1], [0, 0]],  # exterior
...             [[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.2]]  # hole
...         ]
...     },
...     'id': 'building_001',
...     'properties': {
...         'kili': {
...             'categories': [{'name': 'building'}],
...             'children': {},
...             'type': 'semantic'
...         }
...     }
... }
>>> geojson_polygon_feature_to_kili_segmentation_annotation(polygon)
[
    {
        'children': {},
        'boundingPoly': [[
            {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]},
            {'normalizedVertices': [{'x': 0.2, 'y': 0.2}, {'x': 0.8, 'y': 0.2}, {'x': 0.8, 'y': 0.8}]}
        ]],
        'categories': [{'name': 'building'}],
        'mid': 'building_001',
        'type': 'semantic'
    }
]

# MultiPolygon feature
>>> multipolygon = {
...     'type': 'Feature',
...     'geometry': {
...         'type': 'MultiPolygon',
...         'coordinates': [
...             [[[0, 0], [1, 0], [1, 1], [0, 0]]],  # First polygon
...             [[[2, 2], [3, 2], [3, 3], [2, 2]]]   # Second polygon
...         ]
...     },
...     'id': 'forest_001',
...     'properties': {
...         'kili': {
...             'categories': [{'name': 'forest'}],
...             'children': {},
...             'type': 'semantic'
...         }
...     }
... }
>>> geojson_polygon_feature_to_kili_segmentation_annotation(multipolygon)
[
    {
        'children': {},
        'boundingPoly': [
            [{'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}],
            [{'normalizedVertices': [{'x': 2, 'y': 2}, {'x': 3, 'y': 2}, {'x': 3, 'y': 3}]}]
        ],
        'categories': [{'name': 'forest'}],
        'mid': 'forest_001',
        'type': 'semantic'
    }
]
Source code in kili_formats/format/geojson/segmentation.py
def geojson_polygon_feature_to_kili_segmentation_annotation(
    polygon: Dict[str, Any],
    categories: Optional[List[Dict]] = None,
    children: Optional[Dict] = None,
    mid: Optional[str] = None,
) -> List[Dict[str, Any]]:
    """Convert a geojson polygon feature to a list of Kili segmentation annotations.

    Args:
        polygon: A geojson polygon feature.
        categories: The categories of the annotation.
            If not provided, the categories are taken from the `kili` key of the geojson feature properties.
        children: The children of the annotation.
            If not provided, the children are taken from the `kili` key of the geojson feature properties.
        mid: The mid of the annotation.
            If not provided, the mid is taken from the `id` key of the geojson feature.
            If no id is available, a new UUID is generated.

    Returns:
        A list of Kili segmentation annotations. Each annotation has boundingPoly of dimension 2.

        The first dimensions corresponds to the polygon parts (1 for Polygon, N for MultiPolygon).
        The second dimension corresponds to the rings of each polygon part (exterior + holes).

    !!! Example
        ```python
        # Polygon feature
        >>> polygon = {
        ...     'type': 'Feature',
        ...     'geometry': {
        ...         'type': 'Polygon',
        ...         'coordinates': [
        ...             [[0, 0], [1, 0], [1, 1], [0, 0]],  # exterior
        ...             [[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.2]]  # hole
        ...         ]
        ...     },
        ...     'id': 'building_001',
        ...     'properties': {
        ...         'kili': {
        ...             'categories': [{'name': 'building'}],
        ...             'children': {},
        ...             'type': 'semantic'
        ...         }
        ...     }
        ... }
        >>> geojson_polygon_feature_to_kili_segmentation_annotation(polygon)
        [
            {
                'children': {},
                'boundingPoly': [[
                    {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]},
                    {'normalizedVertices': [{'x': 0.2, 'y': 0.2}, {'x': 0.8, 'y': 0.2}, {'x': 0.8, 'y': 0.8}]}
                ]],
                'categories': [{'name': 'building'}],
                'mid': 'building_001',
                'type': 'semantic'
            }
        ]

        # MultiPolygon feature
        >>> multipolygon = {
        ...     'type': 'Feature',
        ...     'geometry': {
        ...         'type': 'MultiPolygon',
        ...         'coordinates': [
        ...             [[[0, 0], [1, 0], [1, 1], [0, 0]]],  # First polygon
        ...             [[[2, 2], [3, 2], [3, 3], [2, 2]]]   # Second polygon
        ...         ]
        ...     },
        ...     'id': 'forest_001',
        ...     'properties': {
        ...         'kili': {
        ...             'categories': [{'name': 'forest'}],
        ...             'children': {},
        ...             'type': 'semantic'
        ...         }
        ...     }
        ... }
        >>> geojson_polygon_feature_to_kili_segmentation_annotation(multipolygon)
        [
            {
                'children': {},
                'boundingPoly': [
                    [{'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}],
                    [{'normalizedVertices': [{'x': 2, 'y': 2}, {'x': 3, 'y': 2}, {'x': 3, 'y': 3}]}]
                ],
                'categories': [{'name': 'forest'}],
                'mid': 'forest_001',
                'type': 'semantic'
            }
        ]
        ```
    """
    assert (
        polygon.get("type") == "Feature"
    ), f"Feature type must be `Feature`, got: {polygon['type']}"

    geometry_type = polygon["geometry"]["type"]
    assert geometry_type in [
        "Polygon",
        "MultiPolygon",
    ], f"Geometry type must be `Polygon` or `MultiPolygon`, got: {geometry_type}"

    children = children or polygon["properties"].get("kili", {}).get("children", {})
    categories = categories or polygon["properties"]["kili"]["categories"]

    annotation_mid = None
    if mid is not None:
        annotation_mid = str(mid)
    elif "id" in polygon:
        annotation_mid = str(polygon["id"])
    else:
        annotation_mid = str(uuid.uuid4())

    coords = polygon["geometry"]["coordinates"]
    annotations = []

    if geometry_type == "Polygon":
        ret = {
            "children": children,
            "categories": categories,
            "type": "semantic",
            "boundingPoly": [
                [
                    {"normalizedVertices": [{"x": coord[0], "y": coord[1]} for coord in ring[:-1]]}
                    for ring in coords
                ]
            ],
            "mid": annotation_mid,
        }

        annotations.append(ret)

    else:
        ret = {
            "children": children,
            "categories": categories,
            "type": "semantic",
            "boundingPoly": [
                [
                    {"normalizedVertices": [{"x": coord[0], "y": coord[1]} for coord in ring[:-1]]}
                    for ring in polygon_coords
                ]
                for polygon_coords in coords
            ],
            "mid": annotation_mid,
        }

        annotations.append(ret)

    return annotations

kili_segmentation_annotation_to_geojson_polygon_feature(segmentation_annotation, job_name=None)

Convert a Kili segmentation annotation to a geojson polygon feature.

Parameters:

Name Type Description Default
segmentation_annotation Dict[str, Any]

A Kili segmentation annotation.

required
job_name Optional[str]

The name of the job to which the annotation belongs.

None

Returns:

Type Description
Dict[str, Any]

A geojson polygon feature (can be Polygon or MultiPolygon).

Example

# Simple polygon annotation
>>> segmentation = {
...     'children': {},
...     'boundingPoly': [
...         [  # Single polygon group
...             {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]},
...             {'normalizedVertices': [{'x': 0.2, 'y': 0.2}, {'x': 0.8, 'y': 0.2}, {'x': 0.8, 'y': 0.8}]}
...         ]
...     ],
...     'categories': [{'name': 'building'}],
...     'mid': 'building_001',
...     'type': 'semantic'
... }
>>> kili_segmentation_annotation_to_geojson_polygon_feature(segmentation, 'detection_job')
{
    'type': 'Feature',
    'geometry': {
        'type': 'Polygon',
        'coordinates': [
            [[0, 0], [1, 0], [1, 1], [0, 0]],
            [[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.2]]
        ]
    },
    'id': 'building_001',
    'properties': {
        'kili': {
            'categories': [{'name': 'building'}],
            'children': {},
            'type': 'semantic',
            'job': 'detection_job'
        }
    }
}

# MultiPolygon annotation
>>> segmentation = {
...     'children': {},
...     'boundingPoly': [
...         [{'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}],  # First polygon
...         [{'normalizedVertices': [{'x': 2, 'y': 2}, {'x': 3, 'y': 2}, {'x': 3, 'y': 3}]}]   # Second polygon
...     ],
...     'categories': [{'name': 'forest'}],
...     'mid': 'forest_001',
...     'type': 'semantic'
... }
>>> kili_segmentation_annotation_to_geojson_polygon_feature(segmentation, 'detection_job')
{
    'type': 'Feature',
    'geometry': {
        'type': 'MultiPolygon',
        'coordinates': [
            [[[0, 0], [1, 0], [1, 1], [0, 0]]],
            [[[2, 2], [3, 2], [3, 3], [2, 2]]]
        ]
    },
    'id': 'forest_001',
    'properties': {
        'kili': {
            'categories': [{'name': 'forest'}],
            'children': {},
            'type': 'semantic',
            'job': 'detection_job'
        }
    }
}

# Flat format annotation
>>> segmentation = {
...     'children': {},
...     'boundingPoly': [
...         {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}
...     ],
...     'categories': [{'name': 'object'}],
...     'mid': 'object_001',
...     'type': 'semantic'
... }
>>> kili_segmentation_annotation_to_geojson_polygon_feature(segmentation, 'detection_job')
{
    'type': 'Feature',
    'geometry': {
        'type': 'Polygon',
        'coordinates': [
            [[0, 0], [1, 0], [1, 1], [0, 0]]
        ]
    },
    'id': 'object_001',
    'properties': {
        'kili': {
            'categories': [{'name': 'object'}],
            'children': {},
            'type': 'semantic',
            'job': 'detection_job'
        }
    }
}
Source code in kili_formats/format/geojson/segmentation.py
def kili_segmentation_annotation_to_geojson_polygon_feature(
    segmentation_annotation: Dict[str, Any], job_name: Optional[str] = None
) -> Dict[str, Any]:
    """Convert a Kili segmentation annotation to a geojson polygon feature.

    Args:
        segmentation_annotation: A Kili segmentation annotation.
        job_name: The name of the job to which the annotation belongs.

    Returns:
        A geojson polygon feature (can be Polygon or MultiPolygon).

    !!! Example
        ```python
        # Simple polygon annotation
        >>> segmentation = {
        ...     'children': {},
        ...     'boundingPoly': [
        ...         [  # Single polygon group
        ...             {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]},
        ...             {'normalizedVertices': [{'x': 0.2, 'y': 0.2}, {'x': 0.8, 'y': 0.2}, {'x': 0.8, 'y': 0.8}]}
        ...         ]
        ...     ],
        ...     'categories': [{'name': 'building'}],
        ...     'mid': 'building_001',
        ...     'type': 'semantic'
        ... }
        >>> kili_segmentation_annotation_to_geojson_polygon_feature(segmentation, 'detection_job')
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Polygon',
                'coordinates': [
                    [[0, 0], [1, 0], [1, 1], [0, 0]],
                    [[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.2]]
                ]
            },
            'id': 'building_001',
            'properties': {
                'kili': {
                    'categories': [{'name': 'building'}],
                    'children': {},
                    'type': 'semantic',
                    'job': 'detection_job'
                }
            }
        }

        # MultiPolygon annotation
        >>> segmentation = {
        ...     'children': {},
        ...     'boundingPoly': [
        ...         [{'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}],  # First polygon
        ...         [{'normalizedVertices': [{'x': 2, 'y': 2}, {'x': 3, 'y': 2}, {'x': 3, 'y': 3}]}]   # Second polygon
        ...     ],
        ...     'categories': [{'name': 'forest'}],
        ...     'mid': 'forest_001',
        ...     'type': 'semantic'
        ... }
        >>> kili_segmentation_annotation_to_geojson_polygon_feature(segmentation, 'detection_job')
        {
            'type': 'Feature',
            'geometry': {
                'type': 'MultiPolygon',
                'coordinates': [
                    [[[0, 0], [1, 0], [1, 1], [0, 0]]],
                    [[[2, 2], [3, 2], [3, 3], [2, 2]]]
                ]
            },
            'id': 'forest_001',
            'properties': {
                'kili': {
                    'categories': [{'name': 'forest'}],
                    'children': {},
                    'type': 'semantic',
                    'job': 'detection_job'
                }
            }
        }

        # Flat format annotation
        >>> segmentation = {
        ...     'children': {},
        ...     'boundingPoly': [
        ...         {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}
        ...     ],
        ...     'categories': [{'name': 'object'}],
        ...     'mid': 'object_001',
        ...     'type': 'semantic'
        ... }
        >>> kili_segmentation_annotation_to_geojson_polygon_feature(segmentation, 'detection_job')
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Polygon',
                'coordinates': [
                    [[0, 0], [1, 0], [1, 1], [0, 0]]
                ]
            },
            'id': 'object_001',
            'properties': {
                'kili': {
                    'categories': [{'name': 'object'}],
                    'children': {},
                    'type': 'semantic',
                    'job': 'detection_job'
                }
            }
        }
        ```
    """
    assert (
        segmentation_annotation["type"] == "semantic"
    ), f"Annotation type must be `semantic`, got: {segmentation_annotation['type']}"

    geometry = kili_segmentation_to_geojson_geometry(segmentation_annotation["boundingPoly"])

    ret = {
        "type": "Feature",
        "geometry": geometry,
    }

    if "mid" in segmentation_annotation:
        ret["id"] = segmentation_annotation["mid"]

    ret["properties"] = {
        "kili": {
            k: v for k, v in segmentation_annotation.items() if k not in ["mid", "boundingPoly"]
        }
    }

    if job_name is not None:
        ret["properties"]["kili"]["job"] = job_name

    return ret

kili_segmentation_to_geojson_geometry(bounding_poly)

Convert a Kili segmentation to a geojson polygon or multipolygon geometry.

Parameters:

Name Type Description Default
bounding_poly List[Any]

A Kili segmentation bounding polygon. Can be either: - Hierarchical: List of polygon groups, each containing rings - Flat: List of ring dictionaries

required

Returns:

Type Description
Dict[str, Any]

A geojson Polygon or MultiPolygon geometry.

Example

# Single polygon with holes (hierarchical structure)
>>> bounding_poly = [
...     [  # First (and only) polygon group
...         {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]},  # exterior
...         {'normalizedVertices': [{'x': 0.2, 'y': 0.2}, {'x': 0.8, 'y': 0.2}, {'x': 0.8, 'y': 0.8}]}  # hole
...     ]
... ]
>>> kili_segmentation_to_geojson_geometry(bounding_poly)
{
    'type': 'Polygon',
    'coordinates': [
        [[0, 0], [1, 0], [1, 1], [0, 0]],  # exterior ring (closed)
        [[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.2]]  # hole (closed)
    ]
}

# MultiPolygon (hierarchical structure)
>>> bounding_poly = [
...     [  # First polygon group
...         {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}
...     ],
...     [  # Second polygon group
...         {'normalizedVertices': [{'x': 2, 'y': 2}, {'x': 3, 'y': 2}, {'x': 3, 'y': 3}]}
...     ]
... ]
>>> kili_segmentation_to_geojson_geometry(bounding_poly)
{
    'type': 'MultiPolygon',
    'coordinates': [
        [[[0, 0], [1, 0], [1, 1], [0, 0]]],  # First polygon
        [[[2, 2], [3, 2], [3, 3], [2, 2]]]   # Second polygon
    ]
}

# Flat structure (single polygon)
>>> bounding_poly = [
...     {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}
... ]
>>> kili_segmentation_to_geojson_geometry(bounding_poly)
{
    'type': 'Polygon',
    'coordinates': [
        [[0, 0], [1, 0], [1, 1], [0, 0]]
    ]
}
Source code in kili_formats/format/geojson/segmentation.py
def kili_segmentation_to_geojson_geometry(bounding_poly: List[Any]) -> Dict[str, Any]:
    """Convert a Kili segmentation to a geojson polygon or multipolygon geometry.

    Args:
        bounding_poly: A Kili segmentation bounding polygon.
                      Can be either:
                      - Hierarchical: List of polygon groups, each containing rings
                      - Flat: List of ring dictionaries

    Returns:
        A geojson Polygon or MultiPolygon geometry.

    !!! Example
        ```python
        # Single polygon with holes (hierarchical structure)
        >>> bounding_poly = [
        ...     [  # First (and only) polygon group
        ...         {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]},  # exterior
        ...         {'normalizedVertices': [{'x': 0.2, 'y': 0.2}, {'x': 0.8, 'y': 0.2}, {'x': 0.8, 'y': 0.8}]}  # hole
        ...     ]
        ... ]
        >>> kili_segmentation_to_geojson_geometry(bounding_poly)
        {
            'type': 'Polygon',
            'coordinates': [
                [[0, 0], [1, 0], [1, 1], [0, 0]],  # exterior ring (closed)
                [[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.2]]  # hole (closed)
            ]
        }

        # MultiPolygon (hierarchical structure)
        >>> bounding_poly = [
        ...     [  # First polygon group
        ...         {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}
        ...     ],
        ...     [  # Second polygon group
        ...         {'normalizedVertices': [{'x': 2, 'y': 2}, {'x': 3, 'y': 2}, {'x': 3, 'y': 3}]}
        ...     ]
        ... ]
        >>> kili_segmentation_to_geojson_geometry(bounding_poly)
        {
            'type': 'MultiPolygon',
            'coordinates': [
                [[[0, 0], [1, 0], [1, 1], [0, 0]]],  # First polygon
                [[[2, 2], [3, 2], [3, 3], [2, 2]]]   # Second polygon
            ]
        }

        # Flat structure (single polygon)
        >>> bounding_poly = [
        ...     {'normalizedVertices': [{'x': 0, 'y': 0}, {'x': 1, 'y': 0}, {'x': 1, 'y': 1}]}
        ... ]
        >>> kili_segmentation_to_geojson_geometry(bounding_poly)
        {
            'type': 'Polygon',
            'coordinates': [
                [[0, 0], [1, 0], [1, 1], [0, 0]]
            ]
        }
        ```
    """
    if not bounding_poly:
        raise ValueError("Empty bounding_poly")

    is_hierarchical = _is_hierarchical_format(bounding_poly)

    if is_hierarchical:
        # Hierarchical format: [ [ {normalizedVertices: [...]}, ... ], ... ]
        if len(bounding_poly) == 1:
            # Single polygon (potentially with holes)
            ret = {"type": "Polygon", "coordinates": []}
            for ring_dict in bounding_poly[0]:
                ring_coords = [
                    [vertex["x"], vertex["y"]] for vertex in ring_dict["normalizedVertices"]
                ]
                # Ensure the first and last points are identical (closed ring)
                if ring_coords and ring_coords[0] != ring_coords[-1]:
                    ring_coords.append(ring_coords[0])
                ret["coordinates"].append(ring_coords)
            return ret
        else:
            # MultiPolygon
            ret = {"type": "MultiPolygon", "coordinates": []}
            for polygon_group in bounding_poly:
                polygon_coords = []
                for ring_dict in polygon_group:
                    ring_coords = [
                        [vertex["x"], vertex["y"]] for vertex in ring_dict["normalizedVertices"]
                    ]
                    # Ensure the first and last points are identical (closed ring)
                    if ring_coords and ring_coords[0] != ring_coords[-1]:
                        ring_coords.append(ring_coords[0])
                    polygon_coords.append(ring_coords)
                ret["coordinates"].append(polygon_coords)
            return ret
    else:
        # Flat format: [ {normalizedVertices: [...]}, ... ]
        # Treat as single polygon with multiple rings (exterior + holes)
        ret = {"type": "Polygon", "coordinates": []}
        for ring_dict in bounding_poly:
            ring_coords = [[vertex["x"], vertex["y"]] for vertex in ring_dict["normalizedVertices"]]
            # Ensure the first and last points are identical (closed ring)
            if ring_coords and ring_coords[0] != ring_coords[-1]:
                ring_coords.append(ring_coords[0])
            ret["coordinates"].append(ring_coords)
        return ret

Collection

Geojson collection module.

_convert_flat_to_hierarchical_format(annotations_group) private

Convert flat format annotations to hierarchical format.

Parameters:

Name Type Description Default
annotations_group

List of semantic annotations with the same mid

required

Returns:

Type Description
Dict[str, Any]

Single annotation with hierarchical boundingPoly structure

Source code in kili_formats/format/geojson/collection.py
def _convert_flat_to_hierarchical_format(annotations_group) -> Dict[str, Any]:
    """Convert flat format annotations to hierarchical format.

    Args:
        annotations_group: List of semantic annotations with the same mid

    Returns:
        Single annotation with hierarchical boundingPoly structure
    """
    if len(annotations_group) == 1:
        # Single annotation - check if it's already hierarchical
        annotation = annotations_group[0]
        if _is_hierarchical_format(annotation["boundingPoly"]):
            return annotation
        else:
            # Convert flat to hierarchical
            new_ann = annotation.copy()
            new_ann["boundingPoly"] = [annotation["boundingPoly"]]
            return new_ann
    else:
        # Multiple annotations with same mid - merge them
        base_ann = annotations_group[0].copy()
        all_bounding_poly = []

        for annotation in annotations_group:
            if _is_hierarchical_format(annotation["boundingPoly"]):
                # Already hierarchical - add each polygon group
                all_bounding_poly.extend(annotation["boundingPoly"])
            else:
                # Flat format - add as single polygon group
                all_bounding_poly.append(annotation["boundingPoly"])

        base_ann["boundingPoly"] = all_bounding_poly
        return base_ann

_flatten_classification_tree(children_dict, json_interface, prefix='') private

Recursively flatten nested classification and transcription children into dot notation.

Parameters:

Name Type Description Default
children_dict Dict[str, Any]

The children dictionary from kili annotation

required
json_interface Optional[Dict[str, Any]]

The project's json interface

required
prefix str

The current path prefix for nested properties

''

Returns:

Type Description
Dict[str, Any]

A flat dictionary with dot-notated keys

Source code in kili_formats/format/geojson/collection.py
def _flatten_classification_tree(
    children_dict: Dict[str, Any],
    json_interface: Optional[Dict[str, Any]],
    prefix: str = "",
) -> Dict[str, Any]:
    """Recursively flatten nested classification and transcription children into dot notation.

    Args:
        children_dict: The children dictionary from kili annotation
        json_interface: The project's json interface
        prefix: The current path prefix for nested properties

    Returns:
        A flat dictionary with dot-notated keys
    """
    flat_props = {}

    for child_job_name, child_data in children_dict.items():
        job_friendly_name = _get_job_friendly_name(json_interface, child_job_name)

        # Build the key with prefix
        key = f"{prefix}.{job_friendly_name}" if prefix else job_friendly_name

        # Handle transcription subjobs (with text field)
        if "text" in child_data:
            flat_props[key] = child_data["text"]
            continue

        # Handle classification subjobs (with categories field)
        if "categories" not in child_data:
            continue

        is_multi_select = _is_multi_select_job(json_interface, child_job_name)
        categories = child_data["categories"]

        if is_multi_select:
            # Multi-select: create array of friendly names
            friendly_categories = [
                _get_category_friendly_name(json_interface, child_job_name, cat.get("name", ""))
                for cat in categories
            ]
            flat_props[key] = friendly_categories

            # Process nested children for each category
            for cat in categories:
                if "children" in cat and cat["children"]:
                    cat_name = _get_category_friendly_name(
                        json_interface, child_job_name, cat.get("name", "")
                    )
                    nested_prefix = f"{key}.{cat_name}"
                    nested_props = _flatten_classification_tree(
                        cat["children"], json_interface, nested_prefix
                    )
                    flat_props.update(nested_props)
        else:
            # Single-select: use string value
            if len(categories) > 0:
                category_name = categories[0].get("name", "")
                friendly_name = _get_category_friendly_name(
                    json_interface, child_job_name, category_name
                )
                flat_props[key] = friendly_name

                # Process nested children
                if "children" in categories[0] and categories[0]["children"]:
                    nested_prefix = f"{key}.{friendly_name}"
                    nested_props = _flatten_classification_tree(
                        categories[0]["children"], json_interface, nested_prefix
                    )
                    flat_props.update(nested_props)

    return flat_props

_flatten_properties_for_gis(kili_properties, job_name, json_interface=None) private

Flatten Kili properties into GIS-friendly format.

Parameters:

Name Type Description Default
kili_properties Dict[str, Any]

The kili properties object from a feature

required
job_name str

The job name for this annotation

required
json_interface Optional[Dict[str, Any]]

Optional json interface for friendly names

None

Returns:

Type Description
A flattened properties dictionary with
  • class: Main category display name
  • Friendly property names instead of job names
  • Nested classifications as dot notation
  • Multi-select as arrays
  • Original kili object preserved
Source code in kili_formats/format/geojson/collection.py
def _flatten_properties_for_gis(
    kili_properties: Dict[str, Any],
    job_name: str,
    json_interface: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Flatten Kili properties into GIS-friendly format.

    Args:
        kili_properties: The kili properties object from a feature
        job_name: The job name for this annotation
        json_interface: Optional json interface for friendly names

    Returns:
        A flattened properties dictionary with:
        - class: Main category display name
        - Friendly property names instead of job names
        - Nested classifications as dot notation
        - Multi-select as arrays
        - Original kili object preserved
    """
    flattened = {}

    # Check if this is a multi-select job
    is_multi_select = _is_multi_select_job(json_interface, job_name)
    job_friendly_name = _get_job_friendly_name(json_interface, job_name)

    # Set class attribute from main category
    if "categories" in kili_properties and kili_properties["categories"]:
        categories = kili_properties["categories"]

        # Get friendly names for all categories
        category_friendly_names = [
            _get_category_friendly_name(json_interface, job_name, cat.get("name", ""))
            for cat in categories
        ]

        # Set class from first category
        if category_friendly_names:
            flattened["class"] = category_friendly_names[0]

        # For root job, add a property with the job's friendly name
        if is_multi_select:
            # Multi-select: array of category names
            flattened[job_friendly_name] = category_friendly_names
        else:
            # Single-select: just the value
            if category_friendly_names:
                flattened[job_friendly_name] = category_friendly_names[0]

        # Process children for each category
        for i, cat in enumerate(categories):
            if "children" in cat and cat["children"]:
                cat_friendly_name = category_friendly_names[i]
                # Build prefix for nested properties
                prefix = f"{job_friendly_name}.{cat_friendly_name}"
                nested_props = _flatten_classification_tree(cat["children"], json_interface, prefix)
                flattened.update(nested_props)

    # Flatten children (subjobs like transcriptions or nested classifications)
    if "children" in kili_properties and kili_properties["children"]:
        # Determine if children belong to a category or are independent
        children_with_prefix = {}
        children_without_prefix = {}

        if "categories" in kili_properties and kili_properties["categories"]:
            # Check each child job to see if it belongs to the category
            categories = kili_properties["categories"]
            first_category_name = categories[0].get("name", "") if categories else ""

            for child_job_name, child_data in kili_properties["children"].items():
                # Check if this child is defined as a child of the first category
                if _is_child_of_category(
                    json_interface, job_name, first_category_name, child_job_name
                ):
                    children_with_prefix[child_job_name] = child_data
                else:
                    children_without_prefix[child_job_name] = child_data

            # Process children that belong to the category (with prefix)
            if children_with_prefix:
                category_friendly_names = [
                    _get_category_friendly_name(json_interface, job_name, cat.get("name", ""))
                    for cat in categories
                ]
                if category_friendly_names:
                    prefix = f"{job_friendly_name}.{category_friendly_names[0]}"
                    flat_children = _flatten_classification_tree(
                        children_with_prefix, json_interface, prefix
                    )
                    flattened.update(flat_children)

            # Process independent children (without prefix)
            if children_without_prefix:
                flat_children = _flatten_classification_tree(
                    children_without_prefix, json_interface
                )
                flattened.update(flat_children)
        else:
            # No categories, process all children without prefix
            flat_children = _flatten_classification_tree(
                kili_properties["children"], json_interface
            )
            flattened.update(flat_children)

    # Preserve original kili object
    flattened["kili"] = kili_properties

    return flattened

_get_category_friendly_name(json_interface, job_name, category_name) private

Get friendly name for a category from json_interface.

Parameters:

Name Type Description Default
json_interface Optional[Dict[str, Any]]

The project's json interface

required
job_name str

The job identifier

required
category_name str

The category identifier (e.g., "CROP")

required

Returns:

Type Description
str

The friendly name from the category or the category_name if not found

Source code in kili_formats/format/geojson/collection.py
def _get_category_friendly_name(
    json_interface: Optional[Dict[str, Any]], job_name: str, category_name: str
) -> str:
    """Get friendly name for a category from json_interface.

    Args:
        json_interface: The project's json interface
        job_name: The job identifier
        category_name: The category identifier (e.g., "CROP")

    Returns:
        The friendly name from the category or the category_name if not found
    """
    if not json_interface or "jobs" not in json_interface:
        return category_name

    job = json_interface["jobs"].get(job_name)
    if not job or "content" not in job or "categories" not in job["content"]:
        return category_name

    category = job["content"]["categories"].get(category_name)
    if not category or "name" not in category:
        return category_name

    return category["name"]

_get_job_friendly_name(json_interface, job_name) private

Get friendly name for a job from json_interface.

Parameters:

Name Type Description Default
json_interface Optional[Dict[str, Any]]

The project's json interface

required
job_name str

The job identifier (e.g., "CLASSIFICATION_JOB")

required

Returns:

Type Description
str

The friendly name (from exportName or instruction) or the job_name if not found

Source code in kili_formats/format/geojson/collection.py
def _get_job_friendly_name(json_interface: Optional[Dict[str, Any]], job_name: str) -> str:
    """Get friendly name for a job from json_interface.

    Args:
        json_interface: The project's json interface
        job_name: The job identifier (e.g., "CLASSIFICATION_JOB")

    Returns:
        The friendly name (from exportName or instruction) or the job_name if not found
    """
    if not json_interface or "jobs" not in json_interface:
        return job_name

    job = json_interface["jobs"].get(job_name)
    if not job:
        return job_name

    # Prefer exportName if available
    if "exportName" in job and job["exportName"]:
        return job["exportName"]

    # Fall back to instruction
    if "instruction" in job and job["instruction"]:
        return job["instruction"]

    return job_name

_group_semantic_annotations_by_mid(annotations) private

Group semantic annotations by their mid (for multi-part polygons).

Source code in kili_formats/format/geojson/collection.py
def _group_semantic_annotations_by_mid(annotations) -> Dict[str, Any]:
    """Group semantic annotations by their mid (for multi-part polygons)."""
    grouped = defaultdict(list)
    for annotation in annotations:
        if annotation.get("type") == "semantic" and "mid" in annotation:
            grouped[annotation["mid"]].append(annotation)
        else:
            # For annotations without mid or non-semantic, treat as individual
            grouped[id(annotation)] = [annotation]  # Use object id as unique key
    return grouped

_is_child_of_category(json_interface, parent_job_name, category_name, child_job_name) private

Check if a child job is defined as a child of a specific category in json_interface.

Parameters:

Name Type Description Default
json_interface Optional[Dict[str, Any]]

The project's json interface

required
parent_job_name str

The parent job identifier

required
category_name str

The category name

required
child_job_name str

The child job identifier to check

required

Returns:

Type Description
bool

True if the child job is listed in the category's children, False otherwise

Source code in kili_formats/format/geojson/collection.py
def _is_child_of_category(
    json_interface: Optional[Dict[str, Any]],
    parent_job_name: str,
    category_name: str,
    child_job_name: str,
) -> bool:
    """Check if a child job is defined as a child of a specific category in json_interface.

    Args:
        json_interface: The project's json interface
        parent_job_name: The parent job identifier
        category_name: The category name
        child_job_name: The child job identifier to check

    Returns:
        True if the child job is listed in the category's children, False otherwise
    """
    if not json_interface or "jobs" not in json_interface:
        return False

    parent_job = json_interface["jobs"].get(parent_job_name)
    if not parent_job or "content" not in parent_job:
        return False

    categories = parent_job["content"].get("categories", {})
    category = categories.get(category_name)
    if not category:
        return False

    children = category.get("children", [])
    return child_job_name in children

_is_hierarchical_format(bounding_poly) private

Check if boundingPoly is in hierarchical format.

Hierarchical: [ [ {normalizedVertices: [...]}, ... ], ... ] Flat: [ {normalizedVertices: [...]}, ... ]

Source code in kili_formats/format/geojson/collection.py
def _is_hierarchical_format(bounding_poly) -> bool:
    """Check if boundingPoly is in hierarchical format.

    Hierarchical: [ [ {normalizedVertices: [...]}, ... ], ... ]
    Flat: [ {normalizedVertices: [...]}, ... ]
    """
    if not bounding_poly or len(bounding_poly) == 0:
        return False

    first_element = bounding_poly[0]

    # If first element is a list, it's hierarchical
    if isinstance(first_element, list):
        return True

    # If first element is a dict with 'normalizedVertices', it's flat
    if isinstance(first_element, dict) and "normalizedVertices" in first_element:
        return False

    # Default to flat format
    return False

_is_multi_select_job(json_interface, job_name) private

Check if a job is multi-select (checkbox input).

Parameters:

Name Type Description Default
json_interface Optional[Dict[str, Any]]

The project's json interface

required
job_name str

The job identifier

required

Returns:

Type Description
bool

True if the job uses checkbox input, False otherwise

Source code in kili_formats/format/geojson/collection.py
def _is_multi_select_job(json_interface: Optional[Dict[str, Any]], job_name: str) -> bool:
    """Check if a job is multi-select (checkbox input).

    Args:
        json_interface: The project's json interface
        job_name: The job identifier

    Returns:
        True if the job uses checkbox input, False otherwise
    """
    if not json_interface or "jobs" not in json_interface:
        return False

    job = json_interface["jobs"].get(job_name)
    if not job or "content" not in job:
        return False

    return job["content"].get("input") == "checkbox"

features_to_feature_collection(features)

Convert a list of features to a feature collection.

Parameters:

Name Type Description Default
features Sequence[Dict]

a list of Geojson features.

required

Returns:

Type Description
Dict[str, Any]

A Geojson feature collection.

Example

>>> features = [
    {
        'type': 'Feature',
        'geometry': {
            'type': 'Point',
            'coordinates': [-79.0, -3.0]},
            'id': '1',
        }
    },
    {
        'type': 'Feature',
        'geometry': {
            'type': 'Point',
            'coordinates': [-79.0, -3.0]},
            'id': '2',
        }
    }
]
>>> features_to_feature_collection(features)
{
    'type': 'FeatureCollection',
    'features': [
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': [-79.0, -3.0]},
                'id': '1',
            }
        },
        {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': [-79.0, -3.0]},
                'id': '2',
            }
        }
    ]
}
Source code in kili_formats/format/geojson/collection.py
def features_to_feature_collection(
    features: Sequence[Dict],
) -> Dict[str, Any]:
    """Convert a list of features to a feature collection.

    Args:
        features: a list of Geojson features.

    Returns:
        A Geojson feature collection.

    !!! Example
        ```python
        >>> features = [
            {
                'type': 'Feature',
                'geometry': {
                    'type': 'Point',
                    'coordinates': [-79.0, -3.0]},
                    'id': '1',
                }
            },
            {
                'type': 'Feature',
                'geometry': {
                    'type': 'Point',
                    'coordinates': [-79.0, -3.0]},
                    'id': '2',
                }
            }
        ]
        >>> features_to_feature_collection(features)
        {
            'type': 'FeatureCollection',
            'features': [
                {
                    'type': 'Feature',
                    'geometry': {
                        'type': 'Point',
                        'coordinates': [-79.0, -3.0]},
                        'id': '1',
                    }
                },
                {
                    'type': 'Feature',
                    'geometry': {
                        'type': 'Point',
                        'coordinates': [-79.0, -3.0]},
                        'id': '2',
                    }
                }
            ]
        }
        ```
    """
    return {"type": "FeatureCollection", "features": list(features)}

geojson_feature_collection_to_kili_json_response(feature_collection)

Convert a Geojson feature collection to a Kili label json response.

Parameters:

Name Type Description Default
feature_collection Dict[str, Any]

a Geojson feature collection.

required

Returns:

Type Description
Dict[str, Any]

A Kili label json response.

Warning

This method requires the kili key to be present in the geojson features' properties. In particular, the kili dictionary of a feature must contain the categories and type of the annotation. It must also contain the job name.

Example

>>> feature_collection = {
    'type': 'FeatureCollection',
    'features': [
        {
            'type': 'Feature',
            'geometry': {
                ...
            },
            'properties': {
                'kili': {
                    'categories': [{'name': 'A'}],
                    'type': 'marker',
                    'job': 'POINT_DETECTION_JOB'
                }
            }
        },
    ]
}
>>> geojson_feature_collection_to_kili_json_response(feature_collection)
{
    'POINT_DETECTION_JOB': {
        'annotations': [
            {
                'categories': [{'name': 'A'}],
                'type': 'marker',
                'point': ...
            }
        ]
    }
}
Source code in kili_formats/format/geojson/collection.py
def geojson_feature_collection_to_kili_json_response(
    feature_collection: Dict[str, Any],
) -> Dict[str, Any]:
    """Convert a Geojson feature collection to a Kili label json response.

    Args:
        feature_collection: a Geojson feature collection.

    Returns:
        A Kili label json response.

    !!! Warning
        This method requires the `kili` key to be present in the geojson features' properties.
        In particular, the `kili` dictionary of a feature must contain the `categories` and `type` of the annotation.
        It must also contain the `job` name.

    !!! Example
        ```python
        >>> feature_collection = {
            'type': 'FeatureCollection',
            'features': [
                {
                    'type': 'Feature',
                    'geometry': {
                        ...
                    },
                    'properties': {
                        'kili': {
                            'categories': [{'name': 'A'}],
                            'type': 'marker',
                            'job': 'POINT_DETECTION_JOB'
                        }
                    }
                },
            ]
        }
        >>> geojson_feature_collection_to_kili_json_response(feature_collection)
        {
            'POINT_DETECTION_JOB': {
                'annotations': [
                    {
                        'categories': [{'name': 'A'}],
                        'type': 'marker',
                        'point': ...
                    }
                ]
            }
        }
        ```
    """
    assert (
        feature_collection["type"] == "FeatureCollection"
    ), f"Feature collection type must be `FeatureCollection`, got: {feature_collection['type']}"

    annotation_tool_to_converter = {
        "rectangle": geojson_polygon_feature_to_kili_bbox_annotation,
        "marker": geojson_point_feature_to_kili_point_annotation,
        "polygon": geojson_polygon_feature_to_kili_polygon_annotation,
        "polyline": geojson_linestring_feature_to_kili_line_annotation,
        "semantic": geojson_polygon_feature_to_kili_segmentation_annotation,
    }

    json_response = {}

    for feature in feature_collection["features"]:
        if feature.get("properties").get("kili", {}).get("job") is None:
            raise ValueError(f"Job name is missing in the GeoJson feature {feature}")

        job_name = feature["properties"]["kili"]["job"]

        if feature.get("geometry") is None:
            # non localised annotation
            if feature.get("properties").get("kili", {}).get("text") is not None:
                # transcription job
                json_response[job_name] = {"text": feature["properties"]["kili"]["text"]}
            elif feature.get("properties").get("kili", {}).get("categories") is not None:
                # classification job
                json_response[job_name] = {
                    "categories": feature["properties"]["kili"]["categories"]
                }
            else:
                raise ValueError("Invalid kili property in non localised feature")
            continue

        geometry_type = feature["geometry"]["type"]

        if geometry_type == "GeometryCollection":
            kili_annotations = geojson_geometrycollection_feature_to_kili_annotations(feature)
        elif geometry_type == "MultiPoint":
            kili_annotations = geojson_multipoint_feature_to_kili_point_annotations(feature)
        elif geometry_type == "MultiLineString":
            kili_annotations = geojson_multilinestring_feature_to_kili_line_annotations(feature)
        else:
            if feature.get("properties").get("kili", {}).get("type") is None:
                raise ValueError(f"Annotation `type` is missing in the GeoJson feature {feature}")

            annotation_tool = feature["properties"]["kili"]["type"]

            if annotation_tool not in annotation_tool_to_converter:
                raise ValueError(f"Annotation tool {annotation_tool} is not supported.")

            kili_annotation = annotation_tool_to_converter[annotation_tool](feature)
            kili_annotations = (
                kili_annotation if isinstance(kili_annotation, list) else [kili_annotation]
            )

        if job_name not in json_response:
            json_response[job_name] = {}
        if "annotations" not in json_response[job_name]:
            json_response[job_name]["annotations"] = []

        json_response[job_name]["annotations"].extend(kili_annotations)

    return json_response

kili_json_response_to_feature_collection(json_response, json_interface=None, flatten_properties=False)

Convert a Kili label json response to a Geojson feature collection.

Parameters:

Name Type Description Default
json_response Dict[str, Any]

a Kili label json response.

required
json_interface Optional[Dict[str, Any]]

Optional json interface for friendly property names.

None
flatten_properties bool

If True, flatten properties for GIS-friendly format.

False

Returns:

Type Description
Dict[str, Any]

A Geojson feature collection.

Example

>>> json_response = {
    'job_1': {
        'annotations': [...]
    },
    'job_2': {
        'annotations': [...]
    }
}
>>> kili_json_response_to_feature_collection(json_response)
{
    'type': 'FeatureCollection',
    'features': [
        {
            'type': 'Feature',
            'geometry': {
                ...
            }
        },
        {
            'type': 'Feature',
            'geometry': {
                ...
            }
        }
    ]
}
Source code in kili_formats/format/geojson/collection.py
def kili_json_response_to_feature_collection(
    json_response: Dict[str, Any],
    json_interface: Optional[Dict[str, Any]] = None,
    flatten_properties: bool = False,
) -> Dict[str, Any]:
    """Convert a Kili label json response to a Geojson feature collection.

    Args:
        json_response: a Kili label json response.
        json_interface: Optional json interface for friendly property names.
        flatten_properties: If True, flatten properties for GIS-friendly format.

    Returns:
        A Geojson feature collection.

    !!! Example
        ```python
        >>> json_response = {
            'job_1': {
                'annotations': [...]
            },
            'job_2': {
                'annotations': [...]
            }
        }
        >>> kili_json_response_to_feature_collection(json_response)
        {
            'type': 'FeatureCollection',
            'features': [
                {
                    'type': 'Feature',
                    'geometry': {
                        ...
                    }
                },
                {
                    'type': 'Feature',
                    'geometry': {
                        ...
                    }
                }
            ]
        }
        ```
    """
    features = []

    annotation_tool_to_converter = {
        "rectangle": kili_bbox_annotation_to_geojson_polygon_feature,  # bbox
        "marker": kili_point_annotation_to_geojson_point_feature,  # point
        "polygon": kili_polygon_annotation_to_geojson_polygon_feature,  # polygon
        "polyline": kili_line_annotation_to_geojson_linestring_feature,  # line
        "semantic": kili_segmentation_annotation_to_geojson_polygon_feature,  # semantic
    }

    jobs_skipped = []
    ann_tools_not_supported = set()
    for job_name, job_response in json_response.items():
        if "text" in job_response:
            feature = kili_transcription_annotation_to_geojson_non_localised_feature(
                job_response, job_name
            )

            # Flatten properties if requested (transcriptions typically don't have nested classifications)
            if flatten_properties and "properties" in feature and "kili" in feature["properties"]:
                feature["properties"] = _flatten_properties_for_gis(
                    feature["properties"]["kili"], job_name, json_interface
                )

            features.append(feature)
            continue

        if "categories" in job_response:
            feature = kili_classification_annotation_to_geojson_non_localised_feature(
                job_response, job_name
            )

            # Flatten properties if requested
            if flatten_properties and "properties" in feature and "kili" in feature["properties"]:
                feature["properties"] = _flatten_properties_for_gis(
                    feature["properties"]["kili"], job_name, json_interface
                )

            features.append(feature)
            continue

        if "annotations" not in job_response:
            jobs_skipped.append(job_name)
            continue

        # Group semantic annotations by mid before processing
        annotations = job_response["annotations"]
        semantic_annotations = [
            annotation for annotation in annotations if annotation.get("type") == "semantic"
        ]
        non_semantic_annotations = [
            annotation for annotation in annotations if annotation.get("type") != "semantic"
        ]

        # Process non-semantic annotations normally
        for annotation in non_semantic_annotations:
            annotation_tool = annotation.get("type")
            if annotation_tool not in annotation_tool_to_converter:
                ann_tools_not_supported.add(annotation_tool)
                continue

            converter = annotation_tool_to_converter[annotation_tool]

            try:
                feature = converter(annotation, job_name=job_name)

                if (
                    flatten_properties
                    and "properties" in feature
                    and "kili" in feature["properties"]
                ):
                    feature["properties"] = _flatten_properties_for_gis(
                        feature["properties"]["kili"], job_name, json_interface
                    )

                features.append(feature)
            except ConversionError as error:
                warnings.warn(
                    error.args[0],
                    stacklevel=2,
                )
                continue

        # Process semantic annotations with grouping
        if semantic_annotations:
            grouped_semantic = _group_semantic_annotations_by_mid(semantic_annotations)

            for mid_or_id, annotations_group in grouped_semantic.items():
                try:
                    # Convert to hierarchical format if needed
                    merged_annotation = _convert_flat_to_hierarchical_format(annotations_group)

                    # Convert to GeoJSON
                    feature = kili_segmentation_annotation_to_geojson_polygon_feature(
                        merged_annotation, job_name=job_name
                    )

                    if (
                        flatten_properties
                        and "properties" in feature
                        and "kili" in feature["properties"]
                    ):
                        feature["properties"] = _flatten_properties_for_gis(
                            feature["properties"]["kili"], job_name, json_interface
                        )

                    features.append(feature)
                except ConversionError as error:
                    warnings.warn(
                        error.args[0],
                        stacklevel=2,
                    )
                    continue
                except Exception as error:
                    warnings.warn(
                        f"Error converting semantic annotation: {error}",
                        stacklevel=2,
                    )
                    continue

    if jobs_skipped:
        warnings.warn(f"Jobs {jobs_skipped} cannot be exported to GeoJson format.", stacklevel=2)
    if ann_tools_not_supported:
        warnings.warn(
            f"Annotation tools {ann_tools_not_supported} are not supported and will be skipped.",
            stacklevel=2,
        )
    return features_to_feature_collection(features)