A Bitmask is a type of annotation on the Encord platform that allows for pixel-wise segmentation of an image, which can be useful when bounding boxes and polygons don’t provide enough precision, or when topologically separate regions belonging to the same class need to be labeled.

Downloading Bitmask annotations from Encord

The Encord platform allows the creation of Bitmask annotations. After labelling is complete, it is possible to download these annotations using the SDK. The following code example illustrates how to download / export and save Bitmask labels:

import cv2
import numpy as np
from encord import EncordUserClient

# Instantiate Encord client by substituting the path to your private key
user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path="<private_key_path>"
)

# Specify a project using project hash
project = user_client.get_project("<project_hash>")

# Obtain labels for the media of interest
label_row = project.list_label_rows_v2()[0]
label_row.initialise_labels()

# Find annotation
# In this example, it is just a first object on a first frame
object_with_bitmask_annotation = label_row.get_frame_views()[
    0
].get_object_instances()[0]

# Get a bitmask annotation
# In this example, it is a first annotation on the object
bitmask_annotation = object_with_bitmask_annotation.get_annotations()[0]

# Convert bitmask to a numpy array
# Obtained array is a binary mask, so to work with it as an image,
# it is necessary to convert it to a different datatype and scale
bitmask = bitmask_annotation.coordinates.to_numpy_array().astype(np.uint8)

bitmask[bitmask == 1] = 255

# And now we can save the mask as a grayscale image
cv2.imwrite("./mask_as_an_image.png", bitmask)

Uploading Bitmask annotations to Encord

If there are pre-existing Bitmask annotations, previously created in Encord or any other software, they can be uploaded to Encord using the SDK. The following code example illustrates how to read a bitmask from a file, and upload it:

import numpy as np
from encord import EncordUserClient
from encord.objects import Object, OntologyStructure
from encord.objects.coordinates import BitmaskCoordinates

# Firstly, we need to prepare the mask itself.
# For simplicity, we can just mask the whole image
# Note, that the size of the mask must be identical to the size of the image
numpy_coordinates = np.ones((512, 512))

# we also need to make sure that the image is in boolean format
numpy_coordinates = numpy_coordinates.astype(bool)

# Now we can upload it with the following steps

# Instantiate Encord client and get a project using project hash
user_client = EncordUserClient.create_with_ssh_private_key("<your_private_key>")
project = user_client.get_project("<project_hash>")

# Obtain labels for the media of interest
# In this case, just a first image of the project
label_row = project.list_label_rows_v2()[0]
label_row.initialise_labels()

# Find a bitmask annotation object in the project ontology
ontology_structure: OntologyStructure = label_row.ontology_structure
bitmask_ontology_object: Object = ontology_structure.get_child_by_title(
    title="My bitmask feature", type_=Object
)

# Create the instance of this object - actual annotation
bitmask_ontology_object_instance = bitmask_ontology_object.create_instance()

# Set the bitmask as coordinates for the annotation
bitmask_ontology_object_instance.set_for_frames(
    # Create coordinates from provided numpy bitmask
    coordinates=BitmaskCoordinates(numpy_coordinates),
    # Add the bounding box to the first frame
    frames=0,
    # There are multiple additional fields that can be set optionally:
    manual_annotation=True,
)

# And assign the object instance to the label row
label_row.add_object_instance(bitmask_ontology_object_instance)

label_row.save()

Decoding Bitmask labels

Bitmask labels exported from Encord are encoded in an rleString. This rleString can be converted to an image or a binary array.

Creating Bitmask images from JSON export

The script below must only be used to decode bitmasks that are exported in the JSON format.

The following script takes your exported JSON file, finds the rleString, and converts it into an image.

# Import dependencies
import json
from pathlib import Path
import numpy as np
from encord.objects.coordinates import BitmaskCoordinates
from PIL import Image

# Read the JSON file.
# Replace labels.json with the path to the JSON file containing your Bitmask
labels = json.loads(Path("labels.json").read_text())
output_folder = Path("output")
output_folder.mkdir(parents=True, exist_ok=True)

# Finds the rleString and saves it to a .png image
found_any = False
for label_row in labels:
    for du in label_row["data_units"].values():
        slice_nums = list(du["labels"].keys())
        for slice_num in slice_nums:
            for obj in du["labels"][slice_num]["objects"]:
                bmc = BitmaskCoordinates.from_dict(obj)
                nparr = np.array(bmc)
                img = Image.fromarray(nparr)
                # img.show()
                # Replace example.png with the name of the output file
                img.save(output_folder / f"mask_{int(slice_num):05d}_{obj['objectHash']}.png")
print("Decoding complete.")

Creating a binary array

The rleStringcan also be converted into an array of 1’s and 0’s using the script below.

from encord.objects.coordinates import BitmaskCoordinates

# Create a numpy array
np_mask = BitmaskCoordinates.from_dict(obj).to_numpy_array() 
print(np_mask.astype(int))

Creating Bitmask images from COCO export

Use the COCO API to decode Bitmasks when using the COCO export format.

from pycocotools.coco import COCO
import numpy as np
from pycocotools import mask as maskUtils
from PIL import Image
import json


# Read the COCO file. Replace 'labels.json' with the path to your COCO format annotations file
coco_annotations_path = 'labels.json'
coco = COCO(coco_annotations_path)

# Get all annotation IDs (you might want to filter or select specific ones)
annIds = coco.getAnnIds()
anns = coco.loadAnns(annIds)

# Decode each rleString and save as a .png image
for i, ann in enumerate(anns):
    if 'segmentation' in ann and isinstance(ann['segmentation'], dict):

        rle = ann['segmentation']
        
        mask = maskUtils.decode(rle)

        img = Image.fromarray(mask * 255).convert('L')  # Convert to grayscale

# Replace example.png with the name of the output file
        img.save("example.png")

print("Decoding complete.")