Developer guide for importing annotations on geospatial data and sample import formats.
Overview
To import annotations in Labelbox, you need to create an annotations payload. In this section, we provide this payload for every supported annotation type.
Annotation payload types
Labelbox supports two formats for the annotations payload:
- Python annotation types (recommended)
- Provides a seamless transition between third-party platforms, machine learning pipelines, and Labelbox.
- Allows you to build annotations locally with local file paths, numpy arrays, or URLs
- Easily convert Python Annotation Type format to NDJSON format to quickly import annotations to Labelbox
- Supports one-level nested classification (radio, checklist, or free-form text) under a tool or classification annotation.
- JSON
- Skips formatting annotation payload in the Labelbox Python annotation type
- Supports any levels of nested classification (radio, checklist, or free-form text) under a tool or classification annotation.
Label import types
Labelbox additionally supports two types of label imports:
- Model-assisted labeling (MAL)
- This workflow allows you to import computer-generated predictions (or simply annotations created outside of Labelbox) as pre-labels on an asset.
- Ground truth
- This workflow functionality allows you to bulk import your ground truth annotations from an external or third-party labeling system into Labelbox Annotate. Using the label import API to import external data is a useful way to consolidate and migrate all annotations into Labelbox as a single source of truth.
Supported annotations
The following annotations are supported for an geospatial data row:
- Radio
- Checklist
- Free-form text
- Point
- Polyline
- Polygon
- Bounding Box
Classifications
Radio (single choice)
radio_annotation = lb_types.ClassificationAnnotation(
name="radio_question",
value=lb_types.Radio(answer =
lb_types.ClassificationAnswer(name = "first_radio_answer")
)
)
radio_annotation_ndjson = {
"name": "radio_question",
"answer": {"name": "first_radio_answer"}
}
Checklist (multiple choice)
checklist_annotation = lb_types.ClassificationAnnotation(
name="checklist_question",
value=lb_types.Checklist(answer = [
lb_types.ClassificationAnswer(name = "first_checklist_answer"),
lb_types.ClassificationAnswer(name = "second_checklist_answer"),
lb_types.ClassificationAnswer(name = "third_checklist_answer")
])
)
checklist_annotation_ndjson = {
"name": "checklist_question",
"answer": [
{"name": "first_checklist_answer"},
{"name": "second_checklist_answer"},
{"name": "third_checklist_answer"},
]
}
Free-form text
text_annotation = lb_types.ClassificationAnnotation(
name = "free_text",
value = lb_types.Text(answer="sample text")
)
text_annotation_ndjson = {
"name": "free_text",
"answer": "sample text",
}
Nested classifications
nested_radio_annotation = lb_types.ClassificationAnnotation(
name="nested_radio_question",
value=lb_types.Radio(
answer=lb_types.ClassificationAnswer(
name="first_radio_answer",
classifications=[
lb_types.ClassificationAnnotation(
name="sub_radio_question",
value=lb_types.Radio(
answer=lb_types.ClassificationAnswer(
name="first_sub_radio_answer"
)
)
)
]
)
)
)
nested_checklist_annotation = lb_types.ClassificationAnnotation(
name="nested_checklist_question",
value=lb_types.Checklist(
answer=[lb_types.ClassificationAnswer(
name="first_checklist_answer",
classifications=[
lb_types.ClassificationAnnotation(
name="sub_checklist_question",
value=lb_types.Checklist(
answer=[lb_types.ClassificationAnswer(
name="first_sub_checklist_answer"
)]
))
]
)]
)
)
nested_radio_annotation_ndjson= {
'name': 'nested_radio_question',
'answer': {
'name': 'first_radio_answer',
'classifications': [
{
'name':'sub_radio_question',
'answer': { 'name' : 'first_sub_radio_answer'}
}
]
}
}
nested_checklist_annotation_ndjson = {
"name": "nested_checklist_question",
"answer": [{
"name": "first_checklist_answer",
"classifications" : [
{
"name": "sub_checklist_question",
"answer": {"name": "first_sub_checklist_answer"}
}
]
}]
}
Point
point_annotation = lb_types.ObjectAnnotation(
name = "point_geo",
value = lb_types.Point(x=-99.20647859573366, y=19.40018029091072),
)
point_annotation_ndjson = {
"name": "point_geo",
"point": {
"x": -99.20647859573366,
"y": 19.40018029091072
}
}
Polyline
# Coordinates in the desired EPSG coordinate system
coords = [
[
-99.20842051506044,
19.40032196622975
],
[
-99.20809864997865,
19.39758963475322
],
[
-99.20758366584778,
19.39776167179227
],
[
-99.20728325843811,
19.3973265189299
]
]
line_points = []
line_points_ndjson = []
for sub in coords:
line_points.append(lb_types.Point(x=sub[0], y=sub[1]))
line_points_ndjson.append({"x":sub[0], "y":sub[1]})
# Python Annotation
polyline_annotation = lb_types.ObjectAnnotation(
name = "polyline_geo",
value = lb_types.Line(points=line_points),
)
# Coordinates in the desired EPSG coordinate system
coords = [
[
-99.20842051506044,
19.40032196622975
],
[
-99.20809864997865,
19.39758963475322
],
[
-99.20758366584778,
19.39776167179227
],
[
-99.20728325843811,
19.3973265189299
]
]
line_points = []
line_points_ndjson = []
for sub in coords:
line_points.append(lb_types.Point(x=sub[0], y=sub[1]))
line_points_ndjson.append({"x":sub[0], "y":sub[1]})
# NDJSON
polyline_annotation_ndjson = {
"name": "polyline_geo",
"line": line_points_ndjson
}
Polygon
polygon_points = []
for sub in coords_polygon:
polygon_points.append(lb_types.Point(x=sub[0], y=sub[1]))
polygon_annotation = lb_types.ObjectAnnotation(
name = "polygon_geo",
value = lb_types.Polygon(points=polygon_points),
)
# Coordinates in the desired EPSG coordinate system
polygon_points_ndjson = []
for sub in coords_polygon:
polygon_points_ndjson.append({"x":sub[0], "y":sub[1]})
polygon_annotation_ndjson = {
"name": "polygon_geo",
"polygon": polygon_points_ndjson
}
coords_polygon = [
[
-99.21042680740356,
19.40036244486966
],
[
-99.2104160785675,
19.40017017124035
],
[
-99.2103409767151,
19.400008256428897
],
[
-99.21014785766603,
19.400008256428897
],
[
-99.21019077301027,
19.39983622176518
],
[
-99.21022295951845,
19.399674306621385
],
[
-99.21029806137086,
19.39951239131646
],
[
-99.2102873325348,
19.399340356128437
],
[
-99.21025514602663,
19.399117722085677
],
[
-99.21024441719057,
19.39892544698541
],
[
-99.2102336883545,
19.39874329141769
],
[
-99.21021223068239,
19.398561135646027
],
[
-99.21018004417421,
19.398399219233365
],
[
-99.21011567115785,
19.39822718286836
],
[
-99.20992255210878,
19.398136104719125
],
[
-99.20974016189577,
19.398085505725305
],
[
-99.20957922935487,
19.398004547302467
],
[
-99.20939683914186,
19.39792358883935
],
[
-99.20918226242067,
19.39786286996558
],
[
-99.20899987220764,
19.397822390703805
],
[
-99.20891404151918,
19.397994427496787
],
[
-99.20890331268312,
19.398176583902874
],
[
-99.20889258384706,
19.398368859888045
],
[
-99.20889258384706,
19.398540896103246
],
[
-99.20890331268312,
19.39872305189756
],
[
-99.20889258384706,
19.39890520748796
],
[
-99.20889258384706,
19.39907724313608
],
[
-99.20889258384706,
19.399259398329956
],
[
-99.20890331268312,
19.399431433603585
],
[
-99.20890331268312,
19.39961358840092
],
[
-99.20890331268312,
19.399785623300048
],
[
-99.20897841453552,
19.399937418648214
],
[
-99.20919299125673,
19.399937418648214
],
[
-99.2093861103058,
19.39991717927664
],
[
-99.20956850051881,
19.39996777770086
],
[
-99.20961141586305,
19.40013981222548
],
[
-99.20963287353517,
19.40032196622975
],
[
-99.20978307724,
19.4004130431554
],
[
-99.20996546745302,
19.40039280384301
],
[
-99.21019077301027,
19.400372564528084
],
[
-99.21042680740356,
19.40036244486966
]
]
Bounding Box
bbox_top_left = lb_types.Point(x= -99.20746564865112, y=19.39799442829336)
bbox_bottom_right = lb_types.Point(x=-99.20568466186523, y=19.39925939999194)
# Python Annotation
bbox_annotation = lb_types.ObjectAnnotation(
name = "bbox_geo",
value = lb_types.Rectangle(start=bbox_top_left, end=bbox_bottom_right)
)
# Coordinates in the desired EPSG coordinate system
coord_object = {
"coordinates" : [[
[
-99.20746564865112,
19.39799442829336
],
[
-99.20746564865112,
19.39925939999194
],
[
-99.20568466186523,
19.39925939999194
],
[
-99.20568466186523,
19.39799442829336
],
[
-99.20746564865112,
19.39799442829336
]
]]
}
# NDJSON
bbox_annotation_ndjson = {
"name" : "bbox_geo",
"bbox" : {
'top': coord_object["coordinates"][0][1][1],
'left': coord_object["coordinates"][0][1][0],
'height': coord_object["coordinates"][0][3][1] - coord_object["coordinates"][0][1][1],
'width': coord_object["coordinates"][0][3][0] - coord_object["coordinates"][0][1][0]
}
}
Mask to bounding box
Additionally, you create a bounding box with a segmentation mask using the code below.
# Let's create another polygon annotation with Python annotation tools that draws the image using cv2 libraries
hsv = cv2.cvtColor(tiled_image_data.value, cv2.COLOR_RGB2HSV)
mask = cv2.inRange(hsv, (25, 50, 25), (100, 150, 255))
kernel = np.ones((15, 20), np.uint8)
mask = cv2.erode(mask, kernel)
mask = cv2.dilate(mask, kernel)
mask_annotation = lb_types.MaskData.from_2D_arr(mask)
mask_data = lb_types.Mask(mask=mask_annotation, color=[255, 255, 255])
h, w, _ = tiled_image_data.value.shape
pixel_bounds = lb_types.TiledBounds(epsg=lb_types.EPSG.SIMPLEPIXEL,
bounds=[lb_types.Point(x=0, y=0),
lb_types.Point(x=w, y=h)])
transformer = lb_types.EPSGTransformer.create_pixel_to_geo_transformer(
src_epsg=pixel_bounds.epsg,
pixel_bounds=pixel_bounds,
geo_bounds=tiled_image_data.tile_bounds,
zoom=20)
pixel_polygons = mask_data.shapely.simplify(3)
list_of_polygons = [transformer(lb_types.Polygon.from_shapely(p)) for p in pixel_polygons.geoms]
polygon_annotation_two = lb_types.ObjectAnnotation(value=list_of_polygons[0], name="polygon_geo_2")
Tool with nested classification
tool_with_radio_subclass_annotation = lb_types.ObjectAnnotation(
name=# Feature name,
value=# Add tool annotation (lb_types."tool"),
classifications=[
lb_types.ClassificationAnnotation(
name="sub_radio_question",
value=lb_types.Radio(answer=lb_types.ClassificationAnswer(
name="first_sub_radio_answer")))
])
bbox_with_radio_subclass_annotation = lb_types.ObjectAnnotation(
name="bbox_with_radio_subclass",
value=lb_types.Rectangle(
start=lb_types.Point(x=541, y=933), # x = left, y = top
end=lb_types.Point(x=871, y=1124), # x= left + width , y = top + height
),
classifications=[
lb_types.ClassificationAnnotation(
name="sub_radio_question",
value=lb_types.Radio(answer=lb_types.ClassificationAnswer(
name="first_sub_radio_answer")))
])
bbox_with_radio_subclass_ndjson = {
"name": "bbox_with_radio_subclass",
"classifications": [{
"name": "sub_radio_question",
"answer": {
"name": "first_sub_radio_answer"
}
}],
"bbox": {
"top": 933,
"left": 541,
"height": 191,
"width": 330
}
}
Example: Import pre-labels or ground truths
The steps to import annotations as pre-labels (machine-assisted learning) are similar to those to import annotations as ground truth labels. However, they vary slightly, and we will describe the differences for each scenario.
Before you start
The below imports are needed to use the code examples in this section.
import uuid
import numpy as np
import cv2
import labelbox as lb
import labelbox.types as lb_types
Replace the value of API_KEY
with a valid API key to connect to the Labelbox client.
API_KEY = None
client = lb.Client(API_KEY)
Step 1: Import data rows
Data rows must first be uploaded to Catalog to attach annotations.
This example shows how to create a data row in Catalog by attaching it to a dataset .
top_left_bound = lb_types.Point(x=-99.21052827588443, y=19.400498983095076)
bottom_right_bound = lb_types.Point(x=-99.20534818927473, y=19.39533555271248)
epsg = lb_types.EPSG.EPSG4326
bounds = lb_types.TiledBounds(epsg=epsg, bounds=[top_left_bound, bottom_right_bound])
global_key = "mexico_city"
tile_layer = lb_types.TileLayer(
url="https://s3-us-west-1.amazonaws.com/lb-tiler-layers/mexico_city/{z}/{x}/{y}.png"
)
tiled_image_data = lb_types.TiledImageData(tile_layer=tile_layer,
tile_bounds=bounds,
zoom_levels=[17, 23])
asset = {
"row_data": tiled_image_data.asdict(),
"global_key": global_key,
"media_type": "TMS_GEO"
}
dataset = client.create_dataset(name="geo_demo_dataset")
task= dataset.create_data_rows([asset])
print("Errors:",task.errors)
print("Failed data rows:", task.failed_data_rows)
Step 2: Set up ontology
Your project ontology should support the tools and classifications required by your annotations. To ensure accurate schema feature mapping, the value used as the name
parameter should match the value of the name
field in your annotation.
For example, when we created an annotation above, we provided a nameannotation_name
. Now, when we set up our ontology, we must ensure that the name of our bounding box tool is also anotations_name
. The same alignment must hold true for the other tools and classifications we create in our ontology.
This example shows how to create an ontology containing all supported annotation types .
ontology_builder = lb.OntologyBuilder(
tools=[
lb.Tool(tool=lb.Tool.Type.POINT, name="point_geo"),
lb.Tool(tool=lb.Tool.Type.LINE, name="polyline_geo"),
lb.Tool(tool=lb.Tool.Type.POLYGON, name="polygon_geo"),
lb.Tool(tool=lb.Tool.Type.POLYGON, name="polygon_geo_2"),
lb.Tool(tool=lb.Tool.Type.BBOX, name="bbox_geo"),
lb.Tool(
tool=lb.Tool.Type.BBOX,
name="bbox_checklist_geo",
classifications=[
lb.Classification(
class_type=lb.Classification.Type.CHECKLIST,
name="checklist_class_name",
options=[
lb.Option(value="first_checklist_answer")
]
),
]
),
lb.Tool(
tool=lb.Tool.Type.BBOX,
name="bbox_text_geo",
classifications=[
lb.Classification(
class_type=lb.Classification.Type.TEXT,
name="free_text_geo"
),
]
)
],
classifications = [
lb.Classification(
class_type=lb.Classification.Type.CHECKLIST,
name="checklist_question_geo",
options=[
lb.Option(value="first_checklist_answer"),
lb.Option(value="second_checklist_answer"),
lb.Option(value="third_checklist_answer")
]
),
lb.Classification(
class_type=lb.Classification.Type.RADIO,
name="radio_question_geo",
options=[
lb.Option(value="first_radio_answer")
]
),
lb.Classification(
class_type=lb.Classification.Type.RADIO,
name="nested_radio_question",
options=[
lb.Option(value="first_radio_answer",
options=[
lb.Classification(
class_type=lb.Classification.Type.RADIO,
name="sub_radio_question",
options=[
lb.Option(value="first_sub_radio_answer")
]
),
]
),
],
),
lb.Classification(class_type=lb.Classification.Type.TEXT,
name="free_text"),
lb.Classification(
class_type=lb.Classification.Type.CHECKLIST,
name="nested_checklist_question",
options=[
lb.Option("first_checklist_answer",
options=[
lb.Classification(
class_type=lb.Classification.Type.CHECKLIST,
name="sub_checklist_question",
options=[lb.Option("first_sub_checklist_answer")]
)
]
)
]
)
]
)
ontology = client.create_ontology("Ontology Geospatial Annotations", ontology_builder.asdict(), media_type=lb.MediaType.Geospatial_Tile)
Step 3: Set Up a Labeling Project
# Create a project
project = client.create_project(
name="Geospatial Project Demo",
media_type=lb.MediaType.Geospatial_Tile
)
# Connect the ontology
project.connect_ontology(ontology)
Step 4: Send Data Rows to Project
# Create a batch to send to your MAL project
batch = project.create_batch(
"first-batch-geo-demo", # Each batch in a project must have a unique name
global_keys=[global_key], # Paginated collection of data row objects, list of data row ids or global keys
priority=5 # priority between 1(Highest) - 5(lowest)
)
print("Batch: ", batch)
Step 5: Create annotation payloads
For help understanding annotation payloads, see overview. To declare payloads, you can use Python annotation types (preferred) or NDJSON objects. For annotations that you want to import as ground truth labels, you can also specify benchmarks using the is_benchmark_reference
flag.
These examples demonstrate each format and how to compose annotations into labels attached to data rows.
labels =[]
labels.append(
lb_types.Label(
data={
"global_key": global_key,
"tile_layer": tile_layer,
"tile_bounds":bounds,
"zoom_levels": [12, 20]
},
annotations = [
point_annotation,
polyline_annotation,
polygon_annotation,
bbox_annotation,
radio_annotation,
bbox_with_checklist_subclass,
bbox_with_free_text_subclass,
checklist_annotation,
polygon_annotation_two,
nested_checklist_annotation,
nested_radio_annotation,
text_annotation
],
# Optional: set the label as a benchmark
# Only supported for groud truth imports
is_benchmark_reference = True
)
)
label_ndjson = []
for annotations in [point_annotation_ndjson,
polyline_annotation_ndjson,
polygon_annotation_ndjson,
bbox_annotation_ndjson,
radio_annotation_ndjson,
bbox_with_checklist_subclass_ndjson,
bbox_with_free_text_subclass_ndjson,
checklist_annotation_ndjson,
nested_checklist_annotation_ndjson,
nested_radio_annotation_ndjson,
text_annotation_ndjson
]:
annotations.update({
'dataRow': {
'globalKey': global_key
}
})
label_ndjson.append(annotations)
Step 6: Import annotation payload
For prelabeled (model-assisted labeling) scenarios, pass your payload as the value of the predictions
parameter. For ground truths, pass the payload to the labels
parameter.
Option A: Upload as prelabels (model assisted labeling)
This option is helpful for speeding up the initial labeling process and reducing the manual labeling workload for high-volume datasets.
# Upload MAL label for this data row in project
upload_job = lb.MALPredictionImport.create_from_objects(
client = client,
project_id = project.uid,
name="mal_job"+str(uuid.uuid4()),
predictions=label
)
print(f"Errors: {upload_job.errors}", )
print(f"Status of uploads: {upload_job.statuses}"
Option B: Upload to a labeling project as ground truth
This option is helpful for loading high-confidence labels from another platform or previous projects that just need review rather than manual labeling effort.
# Upload label for this data row in project
upload_job = lb.LabelImport.create_from_objects(
client = client,
project_id = project.uid,
name="label_import_job"+str(uuid.uuid4()),
labels=label
)
print(f"Errors: {upload_job.errors}", )
print(f"Status of uploads: {upload_job.statuses}")