In this tutorial, you will learn how to programmatically create 3D annotations for volumes and upload them to Supervisely platform.
Supervisely supports different types of shapes/geometries for volume annotation, and now we will consider the primary type - Mask3D.
You can explore other types as well, like Mask (also known as Bitmap), Bounding Box (Rectangle), and Polygon. However, you'll find more information about them in other articles.
Everything you need to reproduce this tutorial is on GitHub: source code, Visual Studio Code configuration, and a shell script for creating virtual env.
Prepare data for annotations
During your work, you can create 3D annotation shapes, and here are a few ways you can do that:
NRRD files
The easiest way to create Mask3D annotation in Supervisely is to use NRRD file with 3D figure that corresponds to the dimensions of the Volume.
You can find an example NRRD file at data/mask/lung.nrrd in the GitHub repository for this tutorial.
NumPy Arrays
Another simple way to create Mask3D annotation is to use NumPy arrays, where values of 1 represent the object and values of 0 represent empty space.
On the right side, you can see a volume with a pink cuboid. Let's represent this volume as an NumPy array.
figure_array = np.zeros((3, 4, 2))
To draw a pink cuboid on it, you need to assign a value of 1 to the necessary cells. In the code below, each cell is indicated by three axes [axis_0, axis_1, axis_2].
In the Python code example section, we will create a NumPy array that represents a foreign body in the lung as a sphere.
Images
You can also use flat mask annotations, such as black and white pictures, to create Mask3D from them. You just need to know which plane and slice it refers to.
You can find an example image file at data/mask/body.png in the GitHub repository for this tutorial.
If your flat annotation doesn't correspond to the dimensions of the plane, you also need to know its PointLocation. This will help to properly apply the mask to the image. This point indicates where the top-left corner of the mask is located, or in other words, the coordinates of the mask's initial position on the canvas or image.
import osimport numpy as npimport cv2from dotenv import load_dotenvimport supervisely as sly# To init API for communicating with Supervisely Instance. # It needs to load environment variables with credentials and workspace IDif sly.is_development():load_dotenv("local.env")load_dotenv(os.path.expanduser("~/supervisely.env"))api = sly.Api()# Check that you did everything right - the API client initialized with the correct credentials and you defined the correct workspace ID in `local.env`file
workspace_id = sly.env.workspace_id()workspace = api.workspace.get_info_by_id(workspace_id)if workspace isNone: sly.logger.warning("You should put correct WORKSPACE_ID value to local.env")raiseValueError(f"Workspace with id={workspace_id} not found")
Create project and upload volumes
Create an empty project with the name "Volumes Demo" with one dataset "CTChest" in your workspace on the server. If a project with the same name exists in your workspace, it will be automatically renamed (Volumes Demo_001, Volumes Demo_002, etc.) to avoid name collisions.
# create empty project and dataset on serverproject_info = api.project.create( workspace.id, name="Volumes Demo", type=sly.ProjectType.VOLUMES, change_name_if_conflict=True,)dataset_info = api.dataset.create(project_info.id, name="CTChest")sly.logger.info(f"Project with id={project_info.id} and dataset with id={dataset_info.id} have been successfully created")# upload NRRD volume as ndarray into datasetvolume_info = api.volume.upload_nrrd_serie_path( dataset_info.id, name="CTChest.nrrd", path="data/CTChest_nrrd/CTChest.nrrd",)
Create annotations and upload into the volume
# create annotation classeslung_class = sly.ObjClass("lung", sly.Mask3D, color=[111, 107, 151])body_class = sly.ObjClass("body", sly.Mask3D, color=[209, 192, 129])tumor_class = sly.ObjClass("tumor", sly.Mask3D, color=[255, 153, 204])# update project meta with new classesapi.project.append_classes(project_info.id, [lung_class, tumor_class, body_class])################################ 1 NRRD file ######################################mask3d_path ="data/mask/lung.nrrd"# create 3D Mask annotation for 'lung' using NRRD file with 3D objectlung_mask = sly.Mask3D.create_from_file(mask3d_path)lung = sly.VolumeObject(lung_class, mask_3d=lung_mask)############################### 2 NumPy array ###################################### create 3D Mask annotation for 'tumor' using NumPy arraytumor_mask = sly.Mask3D(generate_tumor_array())tumor = sly.VolumeObject(tumor_class, mask_3d=tumor_mask)################################## 3 Image ########################################image_path ="data/mask/body.png"# create 3D Mask annotation for 'body' using image filemask = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)# create an empty mask with the same dimensions as the volumebody_mask = sly.Mask3D(np.zeros(volume_info.file_meta["sizes"], np.bool_))# fill this mask with the an image mask for the desired plane. # to avoid errors, use constants: Plane.AXIAL, Plane.CORONAL, Plane.SAGITTALbody_mask.add_mask_2d(mask, plane_name=sly.Plane.AXIAL, slice_index=69, origin=[36, 91])body = sly.VolumeObject(body_class, mask_3d=body_mask)# create volume annotation objectvolume_ann = sly.VolumeAnnotation( volume_info.meta, objects=[lung, tumor, body], spatial_figures=[lung.figure, tumor.figure, body.figure],)# upload VolumeAnnotationapi.volume.annotation.append(volume_info.id, volume_ann)sly.logger.info( f"Annotation has been sucessfully uploaded to the volume {volume_info.name} in dataset with ID={volume_info.dataset_id}"
)
Auxiliary function for generating tumor NumPy array:
defgenerate_tumor_array():""" Generate a NumPy array representing the tumor as a sphere """ width, height, depth = (512,512,139) # volume shape center = np.array([128, 242, 69])# sphere center in the volume radius =25 x, y, z = np.ogrid[:width,:height,:depth]# Calculate the squared distances from each point to the center squared_distances = (x - center[0]) **2+ (y - center[1]) **2+ (z - center[2]) **2# Create a boolean mask by checking if squared distances are less than or equal to the square of the radius tumor_array = squared_distances <= radius**2 tumor_array = tumor_array.astype(np.uint8)return tumor_array
Step 3. Open the repository directory in Visual Studio Code.
code-r.
Step 4. Change ✅ workspace ID ✅ in local.env file by copying the ID from the context menu of the workspace. A new project with annotated videos will be created in the workspace you define:
WORKSPACE_ID=696# ⬅️ change value
Step 5. Start debugging src/main.py
To sum up
In this tutorial, we learned:
What are the types of annotations for Volumes
How to create a project and dataset, upload volume
How to create 3D annotations and upload into volume
How to configure Python development for Supervisely