How to: Convert Labelbox Image Annotations to YOLOV8 format

This post will show a few methods to get Labelbox box annotations to YOLO annotations with Ultralytics.

Feel free to modify these scripts to your needs, but use them at your own risk.

Review this article on how to get YOLO annotations onto Labelbox.

Setup

Inside Labelbox, you must create a matching ontology and project with the data rows you are trying to label to YOLO annotations. Setting that up is outside the scope of this post, but review Labelbox Developer Docs for more information. Once you have this setup, you need to label some data rows to use these functions.

For this tutorial, you must create a mapping with your Labelbox feature name to YOLO class names. This will allow you the flexibility to use an integer or full class name for your YOLO format. Below is a Python dictionary object demonstrating an example of what this would look like:

# {<labelbox_feature_name>: <yolo_class_name>}
class_mapping = {
    "Person": "person",
    "Vehicle": "bus",
}

Export Labelbox Data Rows

Now that you have a project set up, you can use the below scripts to export to bounding boxes, segment masks, or polygon annotations in YOLO format. You will need to either utilize Labelbox export_v2 or export streamable to loop through your data row list and run each data row on your desired functions. Refer to the setup examples later in the tutorial.

Bounding Box

def lb_json_data_row_bbox_to_yolo(data_row: dict[str:str], project_id: str, ontology_mapping: dict[str:str]):
    """Convert export V2 Labelbox data row to YOLOV8 bbox xywh format

    Args:
        data_row (dict[str:str]): Single Labelbox data row from export streamables
        project_id (str): Labelbox project id were data rows where exported from
        ontology_mapping (dict[<labelbox_feature_name>: <yolo_class_name>]): Bbox feature name must match class name given from Labelbox.

    Returns:
        dict["bbox":dict["xywh": list[tuple["<yolo_xywh_format>"]]]: "cls":["<yolo_class_names>"]]
    """

    yolov8_format = {
        "bbox": {"xywh": []},
        "cls": []
    }

    class_list = ontology_mapping.keys()

    labels = data_row["projects"][project_id]["labels"]
    for label in labels:
        for object in label["annotations"]["objects"]:
            if object["annotation_kind"] == "ImageBoundingBox" and object["name"] in class_list:
                x = (object["bounding_box"]["top"] + object["bounding_box"]["width"]) / 2
                y = (object["bounding_box"]["left"] - object["bounding_box"]["height"]) / 2
                height = object["bounding_box"]["height"]
                width = object["bounding_box"]["width"]
                yolov8_format["cls"].append(ontology_mapping[object["name"]])
                yolov8_format["bbox"]["xywh"].append(x, y, width, height)
    return yolov8_format

Segment Mask

import urllib.request
from PIL import Image
import labelbox as lb
import numpy as np

def lb_json_data_row_masks_to_yolo(data_row: dict[str:str], project_id: str, ontology_mapping: dict[str:str], labelbox_client: lb.Client):
    """Convert export V2 Labelbox data row to YOLOV8 binary numpy arrays.

    Args:
        data_row (dict[str:str]): Single Labelbox data row from export streamables or export v2
        project_id (str): Labelbox project id were data rows where exported from
        ontology_mapping (dict[<labelbox_feature_name>: <yolo_class_name>]): Bbox feature name must match class name given from Labelbox.

    Returns:
        dict["masks":dict["numpy":list["<binary_numpy_array>"]]: "cls":["<yolo_class_names>"]]
    """

    yolov8_format = {
        "masks": {"numpy": []},
        "cls": []
    }

    class_list = ontology_mapping.keys()

    labels = data_row["projects"][project_id]["labels"]
    for label in labels:
        for object in label["annotations"]["objects"]:
            if object["annotation_kind"] == "ImageSegmentationMask" and object["name"] in class_list:
                req = urllib.request.Request(object["mask"]["url"], headers=labelbox_client.headers)
                image_np = np.asarray(Image.open(urllib.request.urlopen(req)))
                binary_array = image_np/255
                if np.max(binary_array) != 1:
                    print(object["name"], " is invalid mask")
                    continue
                yolov8_format["cls"].append(ontology_mapping[object["name"]])
                yolov8_format["masks"]["numpy"].append(image_np/255)
    return yolov8_format
            

Polygon

import labelbox as lb

def lb_json_data_row_polygon_to_yolo(data_row: dict[str:str], project_id: str, ontology_mapping: dict[str:str], labelbox_client: lb.Client):
    """Convert export V2 Labelbox data row to YOLOV8 polygon in xy coordinate format

    Args:
        data_row (dict[str:str]): Single Labelbox data row from export streamables or export v2
        project_id (str): Labelbox project id were data rows where exported from
        ontology_mapping (dict[<labelbox_feature_name>: <yolo_class_name>]): Bbox feature name must match class name given from Labelbox.

    Returns:
        dict["masks":dict["numpy":list["<binary_numpy_array>"]]: "cls":["<yolo_class_names>"]]
    """

    yolov8_format = {
        "masks": {"xy": []},
        "cls": []
    }

    class_list = ontology_mapping.keys()

    labels = data_row["projects"][project_id]["labels"]
    for label in labels:
        for object in label["annotations"]["objects"]:
            if object["annotation_kind"] == "ImagePolygon" and object["name"] in class_list:
                coordinates = [{"x": lb_coordinates["x"], "y": lb_coordinates["y"]} for lb_coordinates in object["polygon"]]
                yolov8_format["cls"].append(ontology_mapping[object["name"]])
                yolov8_format["masks"] = {"xy": coordinates}
    return yolov8_format

Example with Export_V2

import labelbox as lb

client = lb.Client("")

PROJECT_ID = ""

project = client.get_project(PROJECT_ID)

task = project.export_v2()
task.wait_till_done()

export_json = task.result

masks = []
polygons = []
bboxes = []

ontology_mapping_masks = {
    "Person_mask": "person",
    "Vehicle_mask": "truck"
}

ontology_mapping_polygon = {
    "Person_polygon": "person",
    "Vehicle_polygon": "truck"
}

ontology_mapping_bboxes = {
    "Person_bbox": "person",
    "Vehicle_bbox": "truck"
}

for data_row in export_json:
    masks.append(lb_json_data_row_masks_to_yolo(data_row, PROJECT_ID, ontology_mapping_masks, client))
    polygons.append(lb_json_data_row_polygon_to_yolo(data_row, PROJECT_ID, ontology_mapping_polygon))
    bboxes.append(lb_json_data_row_bbox_to_yolo(data_row, PROJECT_ID, ontology_mapping_bboxes))

Example with Export Streamable

import labelbox as lb
import json

client = lb.Client("")

PROJECT_ID = ""

client.enable_experimental = True

project = client.get_project(PROJECT_ID)

export_task = project.export()
export_task.wait_till_done()

ontology_mapping_masks = {
    "Person_mask": "person",
    "Vehicle_mask": "truck"
}

ontology_mapping_polygon = {
    "Person_polygon": "person",
    "Vehicle_polygon": "truck"
}

ontology_mapping_bboxes = {
    "Person_bbox": "person",
    "Vehicle_bbox": "truck"
}

masks = []
polygons = []
bboxes = []

def json_stream_handler(output: lb.JsonConverterOutput):
    data_row = json.loads(output.json_str)
    masks.append(lb_json_data_row_masks_to_yolo(data_row, PROJECT_ID, ontology_mapping_masks, client))
    polygons.append(lb_json_data_row_polygon_to_yolo(data_row, PROJECT_ID, ontology_mapping_polygon))
    bboxes.append(lb_json_data_row_bbox_to_yolo(data_row, PROJECT_ID, ontology_mapping_bboxes))


if export_task.has_errors():
    export_task.get_stream(
    converter=lb.JsonConverter(),
    stream_type=lb.StreamType.ERRORS
    ).start(stream_handler=lambda error: print(error))

if export_task.has_result():
    export_json = export_task.get_stream(
    converter=lb.JsonConverter(),
    stream_type=lb.StreamType.RESULT
    ).start(stream_handler=json_stream_handler)

Conclusion

This guide only serves as a starting point for your YOLO annotations. Feel free to provide feedback on how to improve these scripts. Also, note that these functions only apply to image annotations, not video annotations.

1 Like