Skip to content

Adding dump for VOC instance mask. #859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Format selection is possible after clicking on the Upload annotation / Dump anno
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| PNG mask | X | |
| PNG instance mask | X | |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe](http://labelme.csail.mit.edu/Release3.0) | X | X |
Expand Down
129 changes: 92 additions & 37 deletions cvat/apps/annotation/mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,91 @@
"name": "MASK",
"dumpers": [
{
"display_name": "{name} {format} {version}",
"display_name": "{name} (by class) {format} {version}",
"format": "ZIP",
"version": "1.0",
"handler": "dump"
"handler": "dump_by_class"
},
{
"display_name": "{name} (by instance) {format} {version}",
"format": "ZIP",
"version": "1.0",
"handler": "dump_by_instance"
},
],
"loaders": [
],
}

def dump(file_object, annotations):
from zipfile import ZipFile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gachiemchiep, I believe we explained wrong what we want to have. In general in the file we should have two dumpers:

format_spec = {
    "name": "MASK",
    "dumpers": [
        {
            "display_name": "{name} {format} {version}",
            "format": "ZIP",
            "version": "1.0",
            "handler": "dump_by_class"
        },
        {
            "display_name": "{name} (by instance) {format} {version}",
            "format": "ZIP",
            "version": "1.0",
            "handler": "dump_by_instance"
        },
   ],
    "loaders": [
    ],
}

After that instead of one dump function let's create two dump functions dump_by_class and dump_by_instance. Use common dump function to implement both and avoid code cloning. Does it make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic Oh ok. that makes sense now.

Copy link
Contributor Author

@gachiemchiep gachiemchiep Dec 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic I update the change. Please review

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between dump_by_instance and dump_by_class? Can we use a common function to implement both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic the difference is only which mask will be chosen to write into mask images. I think merging both mask and mask instance into the same dumper method as in the commit before was good enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic It's ok. I'll commit the change soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic I understand that you wanted to merge 2 dumper into 1 and add another parameter as method's switch. But after looking at the README.md I don't think I can add another parameter to the dumper method.

- dumper/loader handler functions. Each function should have the following signature:
  ```python
  def dump_handler(file_object, annotations):
  ```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gachiemchiep,

You can because dump in this case will be an internal function which is used to implement real dumper/loader handlers which will have only the required number of parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic I committed the change. It should be fine by now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmanovic Thanks for the advice. I will commit the change soon

import numpy as np
import os
from pycocotools import mask as maskUtils
import matplotlib.image
import io
from collections import OrderedDict

# RGB format, (0, 0, 0) used for background
def genearte_pascal_colormap(size=256):
colormap = np.zeros((size, 3), dtype=int)
ind = np.arange(size, dtype=int)

for shift in reversed(range(8)):
for channel in range(3):
colormap[:, channel] |= ((ind >> channel) & 1) << shift
ind >>= 3

return colormap
MASK_BY_CLASS = 0
MASK_BY_INSTANCE = 1

def convert_box_to_polygon(points):
def convert_box_to_polygon(shape):
xtl = shape.points[0]
ytl = shape.points[1]
xbr = shape.points[2]
ybr = shape.points[3]

return [xtl, ytl, xbr, ytl, xbr, ybr, xtl, ybr]

colormap = genearte_pascal_colormap()
labels = [label[1]["name"] for label in annotations.meta["task"]["labels"] if label[1]["name"] != 'background']
labels.insert(0, 'background')
label_colors = OrderedDict((label, colormap[idx]) for idx, label in enumerate(labels))
def create_mask_colorizer(annotations, colorize_type):
import numpy as np
from collections import OrderedDict

class MaskColorizer:

def __init__(self, annotations, colorize_type):

if colorize_type == MASK_BY_CLASS:
self.colors = self.gen_class_mask_colors(annotations)
elif colorize_type == MASK_BY_INSTANCE:
self.colors = self.gen_instance_mask_colors()

def generate_pascal_colormap(self, size=256):
# RGB format, (0, 0, 0) used for background
colormap = np.zeros((size, 3), dtype=int)
ind = np.arange(size, dtype=int)

for shift in reversed(range(8)):
for channel in range(3):
colormap[:, channel] |= ((ind >> channel) & 1) << shift
ind >>= 3

with ZipFile(file_object, "w") as output_zip:
return colormap

def gen_class_mask_colors(self, annotations):
colormap = self.generate_pascal_colormap()
labels = [label[1]["name"] for label in annotations.meta["task"]["labels"] if label[1]["name"] != 'background']
labels.insert(0, 'background')
label_colors = OrderedDict((label, colormap[idx]) for idx, label in enumerate(labels))

return label_colors

def gen_instance_mask_colors(self):
colormap = self.generate_pascal_colormap()
# The first color is black
instance_colors = OrderedDict((idx, colormap[idx]) for idx in range(len(colormap)))

return instance_colors

return MaskColorizer(annotations, colorize_type)

def dump(file_object, annotations, colorize_type):

from zipfile import ZipFile, ZIP_STORED
import numpy as np
import os
from pycocotools import mask as maskUtils
import matplotlib.image
import io

colorizer = create_mask_colorizer(annotations, colorize_type=colorize_type)
if colorize_type == MASK_BY_CLASS:
save_dir = "SegmentationClass"
elif colorize_type == MASK_BY_INSTANCE:
save_dir = "SegmentationObject"

with ZipFile(file_object, "w", ZIP_STORED) as output_zip:
for frame_annotation in annotations.group_by_frame():
image_name = frame_annotation.name
annotation_name = "{}.png".format(os.path.splitext(os.path.basename(image_name))[0])
Expand All @@ -63,18 +103,33 @@ def convert_box_to_polygon(points):
if not shapes:
continue
shapes = sorted(shapes, key=lambda x: int(x.z_order))
img = np.zeros((height, width, 3))
buf = io.BytesIO()
for shape in shapes:
points = shape.points if shape.type != 'rectangle' else convert_box_to_polygon(shape.points)
img_mask = np.zeros((height, width, 3))
buf_mask = io.BytesIO()
for shape_index, shape in enumerate(shapes):
points = shape.points if shape.type != 'rectangle' else convert_box_to_polygon(shape)
rles = maskUtils.frPyObjects([points], height, width)
rle = maskUtils.merge(rles)
mask = maskUtils.decode(rle)
color = label_colors[shape.label] / 255
idx = (mask > 0)
img[idx] = color
# get corresponding color
if colorize_type == MASK_BY_CLASS:
color = colorizer.colors[shape.label] / 255
elif colorize_type == MASK_BY_INSTANCE:
color = colorizer.colors[shape_index+1] / 255

img_mask[idx] = color

matplotlib.image.imsave(buf, img, format='png')
output_zip.writestr(annotation_name, buf.getvalue())
labels = '\n'.join('{}:{}'.format(label, ','.join(str(i) for i in color)) for label, color in label_colors.items())
# write mask
matplotlib.image.imsave(buf_mask, img_mask, format='png')
output_zip.writestr(os.path.join(save_dir, annotation_name), buf_mask.getvalue())
# Store color map for each class
labels = '\n'.join('{}:{}'.format(label, ','.join(str(i) for i in color)) for label, color in colorizer.colors.items())
output_zip.writestr('colormap.txt', labels)

def dump_by_class(file_object, annotations):

return dump(file_object, annotations, MASK_BY_CLASS)

def dump_by_instance(file_object, annotations):

return dump(file_object, annotations, MASK_BY_INSTANCE)