Saving project labels#

Use this script to save your local labels to your Encord project.

The code uses a couple of utility functions for constructing dictionaries following the structure of Encord label rows and finding ontology dictionaries from the Encord ontology. You can safely skip those details.

Utility code
 17 import uuid
 18 from datetime import datetime
 19 from pathlib import Path
 20 from typing import Dict, Iterable, List, Optional, Tuple, Union
 21
 22 import pytz
 23
 24 GMT_TIMEZONE = pytz.timezone("GMT")
 25 DATETIME_STRING_FORMAT = "%a, %d %b %Y %H:%M:%S %Z"
 26 Point = Union[Tuple[float, float], List[float]]
 27 BBOX_KEYS = {"x", "y", "h", "w"}
 28
 29 # === UTILITIES === #
 30 def __get_timestamp():
 31     now = datetime.now()
 32     new_timezone_timestamp = now.astimezone(GMT_TIMEZONE)
 33     return new_timezone_timestamp.strftime(DATETIME_STRING_FORMAT)
 34
 35
 36 def __lower_snake_case(s: str):
 37     return "_".join(s.lower().split())
 38
 39
 40 def make_object_dict(
 41     ontology_object: dict,
 42     object_data: Union[Point, Iterable[Point], Dict[str, float]] = None,
 43     object_hash: Optional[str] = None,
 44 ) -> dict:
 45     """
 46     :type ontology_object: The ontology object to associate with the ``object_data``.
 47     :type object_data: The data to put in the object dictionary. This has to conform
 48         with the ``shape`` parameter defined in
 49         ``ontology_object["shape"]``.
 50         - ``shape == "point"``: For key-points, the object data should be
 51           a tuple with x, y coordinates as floats.
 52         - ``shape == "bounding_box"``: For bounding boxes, the
 53           ``object_data`` needs to be a dict with info:
 54           {"x": float, "y": float, "h": float, "w": float} specifying the
 55           top right corner of the box (x, y) and the height and width of
 56           the bounding box.
 57         - ``shape in ("polygon", "polyline")``: For polygons and
 58           polylines, the format is an iterable  of points:
 59           [(x, y), ...] specifying (ordered) points in the
 60           polygon/polyline.
 61           If ``object_hash`` is none, a new hash will be generated.
 62     :type object_hash: If you want the object to have the same id across frames (for
 63         videos only), you can specify the object hash, which need to be
 64         an eight-character hex string (e.g., use
 65         ``str(uuid.uuid4())[:8]`` or the ``objectHash`` from an
 66         associated object.
 67     :returns: An object dictionary conforming with the Encord label row data format.
 68     """
 69     if object_hash is None:
 70         object_hash = str(uuid.uuid4())[:8]
 71
 72     timestamp: str = __get_timestamp()
 73     shape: str = ontology_object.get("shape")
 74
 75     object_dict = {
 76         "name": ontology_object["name"],
 77         "color": ontology_object["color"],
 78         "value": __lower_snake_case(ontology_object["name"]),
 79         "createdAt": timestamp,
 80         "createdBy": "robot@cord.tech",
 81         "confidence": 1,
 82         "objectHash": object_hash,
 83         "featureHash": ontology_object["featureNodeHash"],
 84         "lastEditedAt": timestamp,
 85         "lastEditedBy": "robot@encord.com",
 86         "shape": shape,
 87         "manualAnnotation": False,
 88         "reviews": [],
 89     }
 90
 91     if shape in ["polygon", "polyline"]:
 92         # Check type
 93         try:
 94             data_iter = iter(object_data)
 95         except TypeError:
 96             raise ValueError(
 97                 f"The `object_data` for {shape} should be an iterable of points."
 98             )
 99
100         object_dict[shape] = {
101             str(i): {"x": round(x, 4), "y": round(y, 4)}
102             for i, (x, y) in enumerate(data_iter)
103         }
104
105     elif shape == "point":
106         # Check type
107         if not isinstance(object_data, (list, tuple)):
108             raise ValueError(
109                 f"The `object_data` for {shape} should be a list or tuple."
110             )
111
112         if len(object_data) != 2:
113             raise ValueError(
114                 f"The `object_data` for {shape} should have two coordinates."
115             )
116
117         if not isinstance(object_data[0], float):
118             raise ValueError(
119                 f"The `object_data` for {shape} should contain floats."
120             )
121
122         # Make dict
123         object_dict[shape] = {
124             "0": {"x": round(object_data[0], 4), "y": round(object_data[1], 4)}
125         }
126
127     elif shape == "bounding_box":
128         # Check type
129         if not isinstance(object_data, dict):
130             raise ValueError(
131                 f"The `object_data` for {shape} should be a dictionary."
132             )
133
134         if len(BBOX_KEYS.union(set(object_data.keys()))) != 4:
135             raise ValueError(
136                 f"The `object_data` for {shape} should have keys {BBOX_KEYS}."
137             )
138
139         if not isinstance(object_data["x"], float):
140             raise ValueError(
141                 f"The `object_data` for {shape} should float values."
142             )
143
144         # Make dict
145         object_dict["boundingBox"] = {
146             k: round(v, 4) for k, v in object_data.items()
147         }
148
149     return object_dict
150
151
152 def make_classification_dict_and_answer_dict(
153     ontology_class: dict,
154     answers: Union[List[dict], dict, str],
155     classification_hash: Optional[str] = None,
156 ):
157     """
158
159     :type ontology_class: The ontology classification dictionary obtained from the
160                           project ontology.
161     :type answers: The classification option (potentially list) or text answer to apply.
162                    If this is a dictionary, it is interpreted as an option of either a
163                    radio button answer or a checklist answer.
164                    If it is a string, it is interpreted as the actual text answer.
165     :type classification_hash: If a classification should persist with the same id over
166                                multiple frames (for videos), you can reuse the
167                                ``classificationHash`` of a classifications from a
168                                previous frame.
169
170     :returns: A classification and an answer dictionary conforming with the Encord label
171               row data format.
172     """
173     if classification_hash is None:
174         classification_hash = str(uuid.uuid4())[:8]
175
176     if isinstance(answers, dict):
177         answers = [answers]
178
179     if isinstance(answers, list):  # Radio og checklist
180         answers_list: List[dict] = []
181         for answer in answers:
182             try:
183                 attribute = next(
184                     (attr for attr in ontology_class["attributes"])
185                 )
186             except StopIteration:
187                 raise ValueError(
188                     f"Couldn't find answer `{answer['label']}` in the ontology class"
189                 )
190             answers_list.append(
191                 {
192                     "featureHash": answer["featureNodeHash"],
193                     "name": answer["label"],
194                     "value": answer["value"],
195                 }
196             )
197
198     else:  # Text attribute
199         try:
200             attribute = ontology_class
201             answers_list = answers
202         except StopIteration:
203             raise ValueError(
204                 f"Couldn't find ontology with type text for the string answer {answers}"
205             )
206
207     classification_dict = {
208         "classificationHash": classification_hash,
209         "confidence": 1,
210         "createdAt": __get_timestamp(),
211         "createdBy": "robot@encord.com",
212         "featureHash": ontology_class["featureNodeHash"],
213         "manualAnnotation": False,
214         "name": attribute["name"],
215         "reviews": [],
216         "value": __lower_snake_case(attribute["name"]),
217     }
218
219     classification_answer = {
220         "classificationHash": classification_hash,
221         "classifications": [
222             {
223                 "answers": answers_list,
224                 "featureHash": attribute["featureNodeHash"],
225                 "manualAnnotation": False,
226                 "name": attribute["name"],
227                 "value": __lower_snake_case(attribute["name"]),
228             }
229         ],
230     }
231
232     return classification_dict, classification_answer
233
234
235 def find_ontology_object(ontology: dict, encord_name: str):
236     try:
237         obj = next(
238             (
239                 o
240                 for o in ontology["objects"]
241                 if o["name"].lower() == encord_name.lower()
242             )
243         )
244     except StopIteration:
245         raise ValueError(
246             f"Couldn't match Encord ontology name `{encord_name}` to objects in the "
247             f"Encord ontology."
248         )
249     return obj
250
251
252 def __find_option(
253     top_level_classification: dict, encord_option_name: Optional[str]
254 ):
255     if top_level_classification["type"] == "text":
256         # Text classifications do not have options
257         return None
258     try:
259         option = next(
260             (
261                 o
262                 for o in top_level_classification["options"]
263                 if o["label"].lower() == encord_option_name
264             )
265         )
266     except StopIteration:
267         raise ValueError(
268             f"Couldn't match option name {encord_option_name} to any ontology object."
269         )
270     return option
271
272
273 def find_ontology_classification(
274     ontology: dict, local_to_encord_classifications: dict
275 ):
276     encord_name = local_to_encord_classifications["name"]
277     top_level_attribute = None
278     for classification in ontology["classifications"]:
279         for attribute in classification["attributes"]:
280             if attribute["name"].lower() == encord_name.lower():
281                 top_level_attribute = classification
282                 break
283         if top_level_attribute:
284             break
285
286     if top_level_attribute is None:
287         raise ValueError(
288             f"Couldn't match {encord_name} to Encord classification."
289         )
290
291     options = {
292         o[0]: __find_option(top_level_attribute["attributes"][0], o[1])
293         for o in local_to_encord_classifications.get("options", [])
294     }
295     return {"classification": top_level_attribute, "options": options}

Imports and authentication#

First, import dependencies and authenticate a project manager.

310 from encord import EncordUserClient, Project
311 from encord.orm.project import Project as OrmProject
312 from encord.utilities.label_utilities import construct_answer_dictionaries

Note

To interact with Encord, you need to authenticate a client. You can find more details here.

321 # Authentication: adapt the following line to your private key path
322 private_key_path = Path.home() / ".ssh" / "id_ed25519"
323
324 with private_key_path.open() as f:
325     private_key = f.read()
326
327 user_client = EncordUserClient.create_with_ssh_private_key(private_key)
328
329 # Find project to work with based on title.
330 project_orm: OrmProject = next(
331     (
332         p["project"]
333         for p in user_client.get_projects(title_eq="Your project name")
334     )
335 )
336 project: Project = user_client.get_project(project_orm.project_hash)
337
338 ontology = project.ontology

Saving objects#

To save labels to Encord, you take two steps.

  1. Define a map between your local object type identifiers and Encord ontology objects.

  2. Add objects to Encord label rows

1. Defining object mapping#

You need a way to map between your local object identifiers and the objects from the Encord ontology. The mapping in this example is based on the ontology names that were defined when Adding components to a project ontology. You find the Encord ontology object names with the following lines of code:

ontology = project.get_project()["editor_ontology"]
for obj in ontology["objects"]:
    print(f"Type: {obj['shape']:15s} Name: {obj['name']}")

The code will print something similar to this:

Type: polygon         Name: Dog (polygon)
Type: polyline        Name: Snake (polyline)
Type: bounding_box    Name: Tiger (bounding_box)
Type: point           Name: Ant (key-point)

Below, is an example of how to define your own mapping between your local object identifiers and Encord ontology objects. Note that the keys in the dictionary could be any type of keys. So if your local object types are defined by integers, for example, you can use integers as keys.

378 LOCAL_TO_ENCORD_NAMES = {
379     # local object identifier: Encord object name
380     "Dog": "Dog (polygon)",
381     "Snake": "Snake (polyline)",
382     "Tiger": "Tiger (bounding_box)",
383     "Ant": "Ant (key-point)",
384 }
385
386 local_to_encord_ont_objects = {
387     k: find_ontology_object(ontology, v)
388     for k, v in LOCAL_TO_ENCORD_NAMES.items()
389 }

2. Saving objects to Encord#

As the structure of label rows depends on the type of data in the label row, there are separate workflows for videos and image groups.

Saving objects to label rows with videos

Suppose you have the following local data that you want to save to Encord.

402 # A list of objects to save to Encord.
403 local_objects = [
404     {
405         "frame": 0,
406         "objects": [
407             {  # Polygon
408                 "type": "Dog",  # The local object type identifier
409                 # The data of the object
410                 "data": [[0.1, 0.1], [0.2, 0.1], [0.2, 0.2], [0.1, 0.2]],
411                 # If the object is present in multiple images, specify a unique id
412                 # across frames here
413                 "track_id": 0,
414             },
415             {  # Polyline
416                 "type": "Snake",
417                 "data": [[0.3, 0.3], [0.4, 0.3], [0.4, 0.4], [0.3, 0.4]],
418                 "track_id": 1,
419             },
420         ],
421     },
422     {
423         "frame": 3,
424         "objects": [
425             {  # Polyline
426                 "type": "Snake",
427                 "data": [[0.4, 0.4], [0.5, 0.4], [0.5, 0.5], [0.4, 0.5]],
428                 "track_id": 1,
429             },
430             {  # Bounding box
431                 "type": "Tiger",
432                 "data": {"x": 0.7, "y": 0.7, "w": 0.2, "h": 0.2},
433                 "track_id": 2,
434             },
435             {  # Key-point
436                 "type": "Ant",
437                 "data": [0.3, 0.3],
438                 "track_id": 1,
439             },
440         ],
441     },
442     # ...
443 ]

The data is saved by the following code example.

448 # Title of video to which the local objects are associated
449 video_name = "example_video.mp4"
450
451 # Find the label row corresponding to the video associated with the local objects.
452 label_row: dict = next(
453     (lr for lr in project.label_rows if lr["data_title"] == video_name)
454 )
455
456 # Create or fetch details of the label row from Encord.
457 if label_row["label_hash"] is None:
458     label_row: dict = project.create_label_row(label_row["data_hash"])
459 else:
460     label_row: dict = project.get_label_row(label_row["label_hash"])
461
462 # Videos only have one data unit, so fetch the labels of that data unit.
463 encord_labels: dict = next((du for du in label_row["data_units"].values()))[
464     "labels"
465 ]
466
467 # Collection of Encord object_hashes to allow track_ids to persist across frames.
468 object_hash_idx: Dict[int, str] = {}
469
470 for local_frame_level_objects in local_objects:
471     frame: int = local_frame_level_objects["frame"]
472
473     # Note that we will append to list of existing objects in the label row.
474     encord_frame_labels: dict = encord_labels.setdefault(
475         str(frame), {"objects": [], "classifications": []}
476     )
477     # Uncomment this line if you want to overwrite the objects on the platform
478     # encord_frame_labels["objects"] = []
479
480     for local_class in local_frame_level_objects["objects"]:
481         local_obj_type: str = local_class["type"]
482         encord_obj_type: dict = local_to_encord_ont_objects[local_obj_type]
483
484         track_id = local_class.get("track_id")
485         object_hash: Optional[str] = object_hash_idx.get(track_id)
486
487         # Construct Encord object dictionary
488         encord_object: dict = make_object_dict(
489             encord_obj_type, local_class["data"], object_hash=object_hash
490         )
491         # Add to existing objects in this frame.
492         encord_frame_labels["objects"].append(encord_object)
493
494         # Remember object hash for next time.
495         object_hash_idx.setdefault(track_id, encord_object["objectHash"])
496
497 # NB: This call is important to maintain a valid label_row structure!
498 label_row = construct_answer_dictionaries(label_row)
499 project.save_label_row(label_row["label_hash"], label_row)

Saving objects to label rows with image groups

Suppose you have the following local data that you want to save to Encord.

506 # A list of local objects to save to Encord.
507 local_objects = {
508     # Local image name
509     "000001.jpg": [
510         {  # Polygon
511             "type": "Dog",  # The local object type identifier
512             # The data of the object
513             "data": [[0.1, 0.1], [0.2, 0.1], [0.2, 0.2], [0.1, 0.2]],
514             # If the object is present in multiple images, specify a unique id
515             # across frames here
516             "track_id": 0,
517         },
518         {  # Polyline
519             "type": "Snake",
520             "data": [[0.3, 0.3], [0.4, 0.3], [0.4, 0.4], [0.3, 0.4]],
521             "track_id": 1,
522         },
523     ],
524     "000002.jpg": [
525         {  # Polyline
526             "type": "Snake",
527             "data": [[0.4, 0.4], [0.5, 0.4], [0.5, 0.5], [0.4, 0.5]],
528             "track_id": 1,
529         },
530         {  # Bounding box
531             "type": "Tiger",
532             "data": {"x": 0.7, "y": 0.7, "w": 0.2, "h": 0.2},
533             "track_id": 2,
534         },
535         {  # Key-point
536             "name": "Ant",
537             "data": [0.3, 0.3],
538         },
539     ],
540     # ...
541 }

The data is saved by the following code example.

546 # Take any label row, which contains images with names from `local_objects`.
547 label_row = project.label_rows[0]
548
549 # Create or fetch details of the label row.
550 if label_row["label_hash"] is None:
551     label_row = project.create_label_row(label_row["data_hash"])
552 else:
553     label_row = project.get_label_row(label_row["label_hash"])
554
555 # Collection of Encord object_hashes to allow track_ids to persist across frames.
556 object_hash_idx: Dict[int, str] = {}
557
558 # Image groups a variable number of data units so iterate over those.
559 for encord_data_unit in label_row["data_units"].values():
560     if encord_data_unit["data_title"] not in local_objects:
561         continue  # No match for this data unit.
562
563     # Note: The following line will append objects to the list of existing objects on
564     # the Encord platform. To overwrite, existing objects, uncomment this:
565     # encord_data_unit["labels"] = {"objects": [], "classifications": []}
566     encord_labels: dict = encord_data_unit["labels"]
567
568     for local_class in local_objects[encord_data_unit["data_title"]]:
569         local_obj_type: str = local_class["type"]
570         encord_obj_type: dict = local_to_encord_ont_objects[local_obj_type]
571         track_id = local_class.get("track_id")
572         object_hash: Optional[str] = object_hash_idx.get(track_id)
573
574         # Construct Encord object dictionary
575         encord_object: dict = make_object_dict(
576             encord_obj_type, local_class["data"], object_hash=object_hash
577         )
578         # Add to existing objects in this frame.
579         encord_labels["objects"].append(encord_object)
580
581         # Remember object hash for other data units in the image group.
582         object_hash_idx.setdefault(track_id, encord_object["objectHash"])
583
584 # NB: This call is important to maintain a valid label_row structure!
585 label_row = construct_answer_dictionaries(label_row)
586 project.save_label_row(label_row["label_hash"], label_row)

Saving classifications#

The workflow is very similar for classifications. Much of the code will be identical to that above, but with slight modifications, highlighted with # NEW. The steps are:

  1. Define a classification identifier map.

    1. For top-level classifications

    2. additional step: map classification options.

  2. Add classifications to frames or data units for videos and image groups, respectively.

    1. Add classification to labels dictionaries.

    2. additional step: Add option to the label row’s classification_answers.

1. Defining classification mapping#

To define the mapping, you need to know the names of the ontology classifications and their associated options. Use the following code snippet to list the names of the Encord classifications and their options:

ontology = projet_manager.get_project()["editor_ontology"]
for classification in ontology["classifications"]:
    for att in classification["attributes"]:
        options = (
            "No options for text"
            if att["type"] == "text"
            else [o["label"] for o in att["options"]]
        )
        print(f"Type: {att['type']:9s} Name: {att['name']:20s} options: {options}")

This will produce an output similar to the following:

Type: radio     Name: Has Animal (radio)        options: ['yes', 'no']
Type: checklist Name: Other objects (checklist) options: ['person', 'car', 'leash'],
Type: text      Name: Description (text)        options: No options for text

Below, is an example of how to define your own mapping between your “local” classification identifiers and Encord classifications.

636 LOCAL_TO_ENCORD_NAMES: dict = {  # NEW
637     # Local classification identifier
638     "has_animal": {
639         # Encord classification name
640         "name": "Has Animal (radio)",
641         # Tuples of ("local option identifier", "encord option name")
642         "options": [(1, "yes"), (0, "no")],
643     },
644     "other_objects": {
645         "name": "Other objects (checklist)",
646         "options": [(0, "person"), (1, "car"), (2, "leash")],
647     },
648     "description": {
649         "name": "Description (text)",
650         # No options for text
651     },
652 }
653
654 local_to_encord_ont_classifications = {  # NEW
655     k: find_ontology_classification(ontology, v)
656     for k, v in LOCAL_TO_ENCORD_NAMES.items()
657 }

2. Saving classifications to Encord#

As the structure of label rows depends on the type of data in the label row, there are separate workflows for videos and image groups.

Saving classifications to label rows with videos

Suppose you have the following local data that you want to save to Encord.

669 local_classifications = [  # NEW
670     {
671         "frame": 0,
672         "classifications": [
673             {  # Radio type classicifation
674                 "type": "has_animal",  # The local object type identifier
675                 # The data of the object
676                 "data": 0,
677                 # If the _same_ classification is present across multiple images,
678                 # specify a unique id across frames here
679                 "track_id": 0,
680             },
681             {  # Checklist type classification
682                 "type": "other_objects",
683                 "data": [
684                     1,
685                     2,
686                 ],  # Choose both car (local id 1) and leash (local id 2)
687             },
688             {  # Text type classification
689                 "type": "description",
690                 "data": "Your description of the frame",
691             },
692         ],
693     },
694     {
695         "frame": 1,
696         "classifications": [
697             {
698                 "type": "has_animal",
699                 "data": 0,
700                 "track_id": 0,
701             },
702         ],
703     },
704     # ...
705 ]

The data is saved by the following code example.

710 # Title of video for which the objects are associated
711 video_name = "example_video.mp4"
712
713 # Find the label row corresponding to the video that the labels are associated to.
714 label_row: dict = next(
715     (lr for lr in project.label_rows if lr["data_title"] == video_name)
716 )
717
718 # Create or fetch details of the label row.
719 if label_row["label_hash"] is None:
720     label_row: dict = project.create_label_row(label_row["data_hash"])
721 else:
722     label_row: dict = project.get_label_row(label_row["label_hash"])
723
724 # Videos only have one data unit, so fetch the labels of that data unit.
725 encord_labels: dict = next((du for du in label_row["data_units"].values()))[
726     "labels"
727 ]
728 classification_answers = label_row["classification_answers"]  # New
729
730 # Collection of Encord object_hashes to allow track_ids to persist across frames.
731 object_hash_idx: Dict[int, str] = {}
732
733 for local_frame_level_classifications in local_classifications:
734     frame: int = local_frame_level_classifications["frame"]
735
736     # Note that we will append to list of existing objects in the label row.
737     encord_frame_labels: dict = encord_labels.setdefault(
738         str(frame), {"objects": [], "classifications": []}
739     )
740     # Uncomment this line if you want to overwrite the classifications on the platform
741     # encord_frame_labels["classifications"] = []
742
743     for local_class in local_frame_level_classifications["classification"]:
744         local_class_type: str = local_class["type"]
745
746         # NEW start
747         encord_class_info: dict = local_to_encord_ont_classifications[
748             local_class_type
749         ]
750         encord_classification: dict = encord_class_info["classification"]
751         option_map: dict = encord_class_info["options"]
752
753         if not option_map:  # Text classification
754             answers = local_class["data"]
755         elif isinstance(
756             local_class["data"], (list, tuple)
757         ):  # Multi-option checklist
758             answers = [option_map[o] for o in local_class["data"]]
759         else:  # Single option
760             answers = option_map[local_class["data"]]
761         # NEW end
762
763         track_id = local_class.get("track_id")
764         classification_hash: Optional[str] = object_hash_idx.get(track_id)
765
766         # NEW start
767         # Construct Encord object dictionary
768         (
769             encord_class_dict,
770             encord_answers,
771         ) = make_classification_dict_and_answer_dict(
772             encord_classification,
773             answers,
774             classification_hash=classification_hash,
775         )
776
777         # Check if the same annotation already exist, if it exists, replace it with the local annotation
778         frame_classifications = encord_labels[str(frame)]["classifications"]
779         label_already_exist = False
780         for i in range(len(frame_classifications)):
781             if (
782                 frame_classifications[i]["name"]
783                 == encord_classification["name"]
784             ):
785                 classification_answers.pop(
786                     frame_classifications[i]["classificationHash"]
787                 )
788                 frame_classifications[i] = encord_class_dict
789                 label_already_exist = True
790                 break
791         if not label_already_exist:
792             encord_labels[str(frame)]["classifications"].append(
793                 encord_class_dict
794             )
795
796         if classification_hash is None:  # Save answers once for each track id.
797             classification_answers[
798                 encord_class_dict["classificationHash"]
799             ] = encord_answers
800
801         # Remember object hash for next time.
802         object_hash_idx.setdefault(
803             track_id, encord_class_dict["classificationHash"]
804         )
805         # NEW end
806
807 # NB: This call is important to maintain a valid label_row structure!
808 label_row = construct_answer_dictionaries(label_row)
809 project.save_label_row(label_row["label_hash"], label_row)

Saving classification to label rows with image groups

Suppose you have the following local data that you want to save to Encord.

816 # A list of local objects to save to Encord.
817 local_classifications = {
818     # Local image name
819     "000001.jpg": [
820         {  # Radio type classicifation
821             "type": "has_animal",  # The local object type identifier
822             "data": 0,  # The data of the object
823             # If the _same_ classification is present across multiple images,
824             # specify a unique id across frames here
825             "track_id": 0,
826         },
827         {  # Checklist classification
828             "type": "other_objects",
829             "data": [
830                 1,
831                 2,
832             ],  # Choose both car (local id 1) and leash (local id 2)
833         },
834         {  # Text classification
835             "type": "description",
836             "data": "Your description of the frame",
837         },
838     ],
839     # ...
840 }

The data is saved by the following code example.

846 # Take any label row, which contains images from your local dictionary.
847 label_row = project.label_rows[0]
848
849 # Create or fetch details of the label row.
850 if label_row["label_hash"] is None:
851     label_row = project.create_label_row(label_row["data_hash"])
852 else:
853     label_row = project.get_label_row(label_row["label_hash"])
854
855 classification_answers = label_row["classification_answers"]
856
857 # Collection of Encord object_hashes to allow track_ids to persist across frames.
858 object_hash_idx: Dict[int, str] = {}
859
860 # Image groups a variable number of data units so iterate over those.
861 for encord_data_unit in label_row["data_units"].values():
862     if encord_data_unit["data_title"] not in local_objects:
863         continue  # No match for this data unit.
864
865     # Note: The following line will append objects to the list of existing objects on
866     # the Encord platform. To overwrite, existing objects, uncomment this:
867     # encord_data_unit["labels"]["classifications"] = []
868     encord_labels: dict = encord_data_unit["labels"]
869
870     for local_class in local_classifications[encord_data_unit["data_title"]]:
871         local_class_type: str = local_class["type"]
872
873         # NEW start
874         encord_class_info: dict = local_to_encord_ont_classifications[
875             local_class_type
876         ]
877         encord_classification: dict = encord_class_info["classification"]
878         option_map: dict = encord_class_info["options"]
879
880         if not option_map:  # Text classification
881             answers = local_class["data"]
882         elif isinstance(
883             local_class["data"], (list, tuple)
884         ):  # Multi-option checklist
885             answers = [option_map[o] for o in local_class["data"]]
886         else:  # Single option
887             answers = option_map[local_class["data"]]
888         # NEW end
889
890         track_id = local_class.get("track_id")
891         classification_hash: Optional[str] = object_hash_idx.get(track_id)
892
893         # NEW start
894         # Construct Encord object dictionary
895         (
896             encord_class_dict,
897             encord_answers,
898         ) = make_classification_dict_and_answer_dict(
899             encord_classification,
900             answers,
901             classification_hash=classification_hash,
902         )
903         # Add to existing classifications in this frame.
904         encord_labels["classifications"].append(encord_class_dict)
905
906         if classification_hash is None:  # Save answers once for each track id.
907             classification_answers[
908                 encord_class_dict["classificationHash"]
909             ] = encord_answers
910
911         # Remember object hash for next time.
912         object_hash_idx.setdefault(
913             track_id, encord_class_dict["classificationHash"]
914         )
915
916 # NB: This call is important to maintain a valid label_row structure!
917 label_row = construct_answer_dictionaries(label_row)
918 project.save_label_row(label_row["label_hash"], label_row)

Gallery generated by Sphinx-Gallery