Convert JSON
1. Download JSON
- Option 1: Download JSON file directly from the UI using the
Export
tab and drag JSON file into working director - Option 2: Use mdai library to download JSON
Create Client
import mdai
# Get variables from project info tab and user settings
DOMAIN = 'public.md.ai'
YOUR_PERSONAL_TOKEN = 'a1s2d3f4g4h5h59797kllh8vk'
PROJECT_ID = 'MwBe19Br' #project info
mdai_client = mdai.Client(domain=DOMAIN, access_token=YOUR_PERSONAL_TOKEN)
Example output
Successfully authenticated to public.md.ai.
Download annotations
# Download only the annotation data
p = mdai_client.project(PROJECT_ID, path='.', annotations_only=True)
Example output
Using working directory for data.
Preparing annotations export for project MwBe19Br...
Success: annotations data for project MwBe19Br ready.
Downloading file: mdai_public_project_MwBe19Br_annotations_labelgroup_all_2020-09-23-214038.json
No project created. Downloaded annotations only.
2. JSON to Dataframe
Copy the downloaded file name from the output above or from your downloaded JSON file
#Replace with your filename
JSON = 'mdai_public_project_MwBe19Br_annotations_labelgroup_all_2020-09-23-214038.json'
results = mdai.common_utils.json_to_dataframe(JSON)
#Annotations dataframe
annots_df = results['annotations']
3. Conversions
Bounding box
Extract box data dictionary items
#Simplify table
columns_brief = ['id', 'StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID', 'labelName', 'data', 'annotationMode']
annots_df = annots_df[columns_brief]
#Box annotations
boxes = annots_df[annots_df.annotationMode == 'bbox']
#Extract box data
def extract_box_data(df):
j = df.copy()
j = j[(j.annotationMode == 'bbox') & (~j.data.isnull())]
try:
j['data'] = j['data'].apply(lambda x:json.loads(x.replace("'", "\"")))
except:
j['data']
j['x'] = [d['x'] for _,d in j.data.iteritems()]
j['y'] = [d['y'] for _,d in j.data.iteritems()]
j['w'] = [d['width'] for _,d in j.data.iteritems()]
j['h'] = [d['height'] for _,d in j.data.iteritems()]
j = j.drop('data', axis=1)
return j
boxes = extract_box_data(boxes)
boxes.head()
Example output
id StudyInstanceUID SeriesInstanceUID SOPInstanceUID labelName annotationMode x y w h
0 A_J07mWn 1.3.6.1.4.1.14519.5.2.1.1079.4008.312038908377... 1.3.6.1.4.1.14519.5.2.1.1079.4008.228089024512... 1.3.6.1.4.1.14519.5.2.1.1079.4008.515564548072... Stomach bbox 220.822845 124.504723 121.008514 82.380470
1 A_qxVdNk 1.3.6.1.4.1.14519.5.2.1.1079.4008.312038908377... 1.3.6.1.4.1.14519.5.2.1.1079.4008.228089024512... 1.3.6.1.4.1.14519.5.2.1.1079.4008.306261432947... Stomach bbox 183.308075 148.138794 99.586609 68.920502
2 A_49KLWk 1.3.6.1.4.1.14519.5.2.1.1079.4008.312038908377... 1.3.6.1.4.1.14519.5.2.1.1079.4008.228089024512... 1.3.6.1.4.1.14519.5.2.1.1079.4008.132057312174... Stomach bbox 251.314133 125.590210 84.764328 103.895081
Derive additional box data
Now lets get the box center, area, and bottom left corner
boxes['area'] = boxes.x * boxes.y
boxes['center_x'] = boxes.x + boxes.w/2
boxes['center_y'] = boxes.y + boxes.h/2
boxes['bottom_x'] = boxes.x + boxes.w
boxes['bottom_y'] = boxes.y + boxes.h
#Convert values to integers for simplicity
boxes[boxes.columns[6:]] = boxes[boxes.columns[6:]].astype('int')
boxes.head()
Example output
id StudyInstanceUID SeriesInstanceUID SOPInstanceUID labelName annotationMode x y w h area center_x center_y bottom_x bottom_y
0 A_J07mWn 1.3.6.1.4.1.14519.5.2.1.1079.4008.312038908377... 1.3.6.1.4.1.14519.5.2.1.1079.4008.228089024512... 1.3.6.1.4.1.14519.5.2.1.1079.4008.515564548072... Stomach bbox 220 124 121 82 27391 280 165 341 206
1 A_qxVdNk 1.3.6.1.4.1.14519.5.2.1.1079.4008.312038908377... 1.3.6.1.4.1.14519.5.2.1.1079.4008.228089024512... 1.3.6.1.4.1.14519.5.2.1.1079.4008.306261432947... Stomach bbox 183 148 99 68 27109 232 182 282 217
2 A_49KLWk 1.3.6.1.4.1.14519.5.2.1.1079.4008.312038908377... 1.3.6.1.4.1.14519.5.2.1.1079.4008.228089024512... 1.3.6.1.4.1.14519.5.2.1.1079.4008.132057312174... Stomach bbox 251 125 84 103 31523 293 177 335 229
Formatting examples
Detectron 2
Detectron 2 expects bbox coordinates to be in the format of [x_upper_left, y_upper_left, x_lower_right, y_lower_right]. Be mindful of the need to scale the annotation data if image is scaled
scale = 1.
boxes['detectron_bbox'] = [[row.x * scale, row.y * scale, row.bottom_x * scale, row.bottom_y * scale] for _,row in boxes.iterrows()]
Fastai
Fastai expects bbox coordinates to be in the format of (y_upper_left, x_upper_left, y_lower_right, x_lower_right) with the origin being in the upper left hand corner of the image. Remember to scale annotations if images are scaled
scale = 1.
boxes['fastai_bbox'] = [[row.y * scale, row.x * scale, row.bottom_y * scale, row.bottom_x * scale] for _,row in boxes.iterrows()]
Freeform and polygon
Filter for freeform and polygon
#Simplify table
columns_brief = ['id', 'StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID', 'labelName', 'data', 'annotationMode']
annots_df = annots_df[columns_brief]
#Shape annotations
shapes = annots_df[(annots_df.annotationMode == 'freeform') | (annots_df.annotationMode == 'polygon')]
Get box bounding the vertices
#Extract box data from vertices
import numpy as np
def vertices_to_boxes(data):
vertices = data['vertices']
px=[v[0] for v in vertices]
py=[v[1] for v in vertices]
x = int(np.min(px))
y = int(np.min(py))
x2 = int(np.max(px))
y2 = int(np.max(py))
w = x2 - x
h = y2 - y
return (x, y, w, h, x2, y2)
shapes['x'],shapes['y'],shapes['w'],shapes['h'],shapes['bottom_x'],shapes['bottom_y'] = zip(*shapes['data'].map(vertices_to_boxes))
shapes.head()
Example output
id StudyInstanceUID SeriesInstanceUID SOPInstanceUID labelName data annotationMode x y w h bottom_x bottom_y
0 A_JddPQE 1.3.46.670589.11.20182.5.0.8336.20130724083604... 1.3.46.670589.11.20182.5.0.7188.20130724085044... 1.3.6.1.4.1.9590.100.1.2.260616927513309992437... Liver {'vertices': [[64, 130], [64, 129], [64, 128],... freeform 54 115 10 20 64 135
1 A_qG28ZY 1.3.46.670589.11.20182.5.0.8336.20130724083604... 1.3.46.670589.11.20182.5.0.7188.20130724085044... 1.3.6.1.4.1.9590.100.1.2.388757691711551811514... Liver {'vertices': [[66, 129], [66, 128], [66, 127],... freeform 53 112 13 27 66 139
Longest diameter
Get longest diameter of a shape in mm. Pixel spacing is obtained using the pydicom library and the PixelSpacing tag
import cv2
def longest_diameter(data, pixel_spacing):
try:
row_spacing, col_spacing = pixel_spacing
except:
return -1
max_points = most_distant_points(np.array(data['vertices']))
x1 = max_points[0][0]
y1 = max_points[0][1]
x2 = max_points[1][0]
y2 = max_points[1][1]
dx = col_spacing * (x2 - x1)
dy = row_spacing * (y2 - y1)
distance = np.sqrt(dx ** 2 + dy ** 2)
#returns longest diameter in mm
return round(distance, 2)
def most_distant_points(points):
hull = cv2.convexHull(points, returnPoints=True)
max_points = [points[0], points[1]]
max_distance = 0
for i in range(0,len(hull)):
for j in range(i+1,len(hull)):
p1 = hull[i][0]
p2 = hull[j][0]
distance = calc_distance(p1, p2)
if (distance > max_distance):
max_points = [p1, p2]
max_distance = distance
return max_points;
def calc_distance(p1, p2):
return np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
Example
import pydicom as py
ds = py.dcmread('000.dcm')
ld = longest_diameter(data, ds.PixelSpacing)
Mask
Convert MD.ai annotation to mask
Function to load a single mask instance from one row of annotation data. This will turn one box, free form, polygon, etc into a binary mask sized to the corresponding image.
def load_mask_instance(row):
"""Load instance masks for the given annotation row. Masks can be different types,
mask is a binary true/false map of the same size as the image.
"""
mask = np.zeros((row.height, row.width), dtype=np.uint8)
annotation_mode = row.annotationMode
# print(annotation_mode)
if annotation_mode == "bbox":
# Bounding Box
x = int(row["data"]["x"])
y = int(row["data"]["y"])
w = int(row["data"]["width"])
h = int(row["data"]["height"])
mask_instance = mask[:,:].copy()
cv2.rectangle(mask_instance, (x, y), (x + w, y + h), 255, -1)
mask[:,:] = mask_instance
# FreeForm or Polygon
elif annotation_mode == "freeform" or annotation_mode == "polygon":
vertices = np.array(row["data"]["vertices"])
vertices = vertices.reshape((-1, 2))
mask_instance = mask[:,:].copy()
cv2.fillPoly(mask_instance, np.int32([vertices]), (255, 255, 255))
mask[:,:] = mask_instance
# Line
elif annotation_mode == "line":
vertices = np.array(row["data"]["vertices"])
vertices = vertices.reshape((-1, 2))
mask_instance = mask[:,:].copy()
cv2.polylines(mask_instance, np.int32([vertices]), False, (255, 255, 255), 12)
mask[:,:] = mask_instance
elif annotation_mode == "location":
# Bounding Box
x = int(row["data"]["x"])
y = int(row["data"]["y"])
mask_instance = mask[:,:].copy()
cv2.circle(mask_instance, (x, y), 7, (255, 255, 255), -1)
mask[:,:] = mask_instance
elif annotation_mode is None:
print("Not a local instance")
return mask.astype(np.bool)