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
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. (if errors occur, try downloading annotations outside of your firewall)
# 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
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)