Az/multiformat downloader (#551)
parent
efa47a3aa3
commit
5733423b13
@ -0,0 +1,155 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
The purpose of this application is to add support for multiple annotation formats for CVAT.
|
||||||
|
It allows to download and upload annotations in different formats and easily add support for new.
|
||||||
|
|
||||||
|
## How to add a new annotation format support
|
||||||
|
|
||||||
|
1. Write a python script that will be executed via exec() function. Following items must be defined inside at code:
|
||||||
|
- **format_spec** - a dictionary with the following structure:
|
||||||
|
```python
|
||||||
|
format_spec = {
|
||||||
|
"name": "CVAT",
|
||||||
|
"dumpers": [
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version} for videos",
|
||||||
|
"format": "XML",
|
||||||
|
"version": "1.1",
|
||||||
|
"handler": "dump_as_cvat_interpolation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version} for images",
|
||||||
|
"format": "XML",
|
||||||
|
"version": "1.1",
|
||||||
|
"handler": "dump_as_cvat_annotation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"loaders": [
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version}",
|
||||||
|
"format": "XML",
|
||||||
|
"version": "1.1",
|
||||||
|
"handler": "load",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **name** - unique name for each format
|
||||||
|
- **dumpers and loaders** - lists of objects that describes exposed dumpers and loaders and must
|
||||||
|
have following keys:
|
||||||
|
1. display_name - **unique** string used as ID for a dumpers and loaders.
|
||||||
|
Also this string is displayed in CVAT UI.
|
||||||
|
Possible to use a named placeholders like the python format function
|
||||||
|
(supports only name, format and version variables).
|
||||||
|
1. format - a string, used as extension for a dumped annotation.
|
||||||
|
1. version - just string with version.
|
||||||
|
1. handler - function that will be called and should be defined at top scope.
|
||||||
|
- dumper/loader handler functions. Each function should have the following signature:
|
||||||
|
```python
|
||||||
|
def dump_handler(file_object, annotations):
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside of the script environment 3 variables are available:
|
||||||
|
- file_object - python's standard file object returned by open() function and exposing a file-oriented API
|
||||||
|
(with methods such as read() or write()) to an underlying resource.
|
||||||
|
- **annotations** - instance of [Annotation](annotation.py#L106) class.
|
||||||
|
- **spec** - string with name of the requested specification
|
||||||
|
(if the annotation format defines them).
|
||||||
|
It may be useful if one script implements more than one format support.
|
||||||
|
|
||||||
|
Annotation class expose API and some additional pre-defined types that allow to get/add shapes inside
|
||||||
|
a parser/dumper code.
|
||||||
|
|
||||||
|
Short description of the public methods:
|
||||||
|
- **Annotation.shapes** - property, returns a generator of Annotation.LabeledShape objects
|
||||||
|
- **Annotation.tracks** - property, returns a generator of Annotation.Track objects
|
||||||
|
- **Annotation.tags** - property, returns a generator of Annotation.Tag objects
|
||||||
|
- **Annotation.group_by_frame()** - method, returns an iterator on Annotation.Frame object,
|
||||||
|
which groups annotation objects by frame. Note that TrackedShapes will be represented as Annotation.LabeledShape.
|
||||||
|
- **Annotation.meta** - property, returns dictionary which represent a task meta information,
|
||||||
|
for example - video source name, number of frames, number of jobs, etc
|
||||||
|
- **Annotation.add_tag(tag)** - tag should be a instance of the Annotation.Tag class
|
||||||
|
- **Annotation.add_shape(shape)** - shape should be a instance of the Annotation.Shape class
|
||||||
|
- **Annotation.add_track(track)** - track should be a instance of the Annotation.Track class
|
||||||
|
- **Annotation.Attribute** = namedtuple('Attribute', 'name, value')
|
||||||
|
- name - String, name of the attribute
|
||||||
|
- value - String, value of the attribute
|
||||||
|
- **Annotation.LabeledShape** = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes,
|
||||||
|
group, z_order')
|
||||||
|
LabeledShape.\__new\__.\__defaults\__ = (0, None)
|
||||||
|
- **TrackedShape** = namedtuple('TrackedShape', 'type, points, occluded, frame, attributes, outside,
|
||||||
|
keyframe, z_order')
|
||||||
|
TrackedShape.\__new\__.\__defaults\__ = (None, )
|
||||||
|
- **Track** = namedtuple('Track', 'label, group, shapes')
|
||||||
|
- **Tag** = namedtuple('Tag', 'frame, label, attributes, group')
|
||||||
|
Tag.\__new\__.\__defaults\__ = (0, )
|
||||||
|
- **Frame** = namedtuple('Frame', 'frame, name, width, height, labeled_shapes, tags')
|
||||||
|
|
||||||
|
Pseudocode for a dumper script
|
||||||
|
```python
|
||||||
|
...
|
||||||
|
# dump meta info if necessary
|
||||||
|
...
|
||||||
|
|
||||||
|
# iterate over all frames
|
||||||
|
for frame_annotation in annotations.group_by_frame():
|
||||||
|
# get frame info
|
||||||
|
image_name = frame_annotation.name
|
||||||
|
image_width = frame_annotation.width
|
||||||
|
image_height = frame_annotation.height
|
||||||
|
|
||||||
|
# iterate over all shapes on the frame
|
||||||
|
for shape in frame_annotation.labeled_shapes:
|
||||||
|
label = shape.label
|
||||||
|
xtl = shape.points[0]
|
||||||
|
ytl = shape.points[1]
|
||||||
|
xbr = shape.points[2]
|
||||||
|
ybr = shape.points[3]
|
||||||
|
|
||||||
|
# iterate over shape attributes
|
||||||
|
for attr in shape.attributes:
|
||||||
|
attr_name = attr.name
|
||||||
|
attr_value = attr.value
|
||||||
|
...
|
||||||
|
# dump annotation code
|
||||||
|
file_object.write(...)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
Pseudocode for a parser code
|
||||||
|
```python
|
||||||
|
...
|
||||||
|
#read file_object
|
||||||
|
...
|
||||||
|
|
||||||
|
for parsed_shape in parsed_shapes:
|
||||||
|
shape = annotations.LabeledShape(
|
||||||
|
type="rectangle",
|
||||||
|
points=[0, 0, 100, 100],
|
||||||
|
occluded=False,
|
||||||
|
attributes=[],
|
||||||
|
label="car",
|
||||||
|
outside=False,
|
||||||
|
frame=99,
|
||||||
|
)
|
||||||
|
|
||||||
|
annotations.add_shape(shape)
|
||||||
|
```
|
||||||
|
Full examples can be found in [builtin](builtin) folder.
|
||||||
|
1. Add path to a new python script to the annotation app settings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
BUILTIN_FORMATS = (
|
||||||
|
os.path.join(path_prefix, 'cvat.py'),
|
||||||
|
os.path.join(path_prefix,'pascal_voc.py'),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ideas for improvements
|
||||||
|
|
||||||
|
- Annotation format manager like DL Model manager with which the user can add custom format support by
|
||||||
|
writing dumper/loader scripts.
|
||||||
|
- Often a custom loader/dumper requires additional python packages and it would be useful if CVAT provided some API
|
||||||
|
that allows the user to install a python dependencies from their own code without changing the source code.
|
||||||
|
Possible solutions: install additional modules via pip call to a separate directory for each Annotation Format
|
||||||
|
to reduce version conflicts, etc. Thus, custom code can be run in an extended environment, and core CVAT modules
|
||||||
|
should not be affected. As well, this functionality can be useful for Auto Annotation module.
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
default_app_config = 'cvat.apps.annotation.apps.AnnotationConfig'
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
@ -0,0 +1,410 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
import copy
|
||||||
|
from collections import OrderedDict, namedtuple
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from cvat.apps.engine.data_manager import DataManager, TrackManager
|
||||||
|
from cvat.apps.engine.serializers import LabeledDataSerializer
|
||||||
|
|
||||||
|
class AnnotationIR:
|
||||||
|
def __init__(self, data=None):
|
||||||
|
self.reset()
|
||||||
|
if data:
|
||||||
|
self._tags = getattr(data, 'tags', []) or data['tags']
|
||||||
|
self._shapes = getattr(data, 'shapes', []) or data['shapes']
|
||||||
|
self._tracks = getattr(data, 'tracks', []) or data['tracks']
|
||||||
|
|
||||||
|
def add_tag(self, tag):
|
||||||
|
self._tags.append(tag)
|
||||||
|
|
||||||
|
def add_shape(self, shape):
|
||||||
|
self._shapes.append(shape)
|
||||||
|
|
||||||
|
def add_track(self, track):
|
||||||
|
self._tracks.append(track)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
return self._tags
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shapes(self):
|
||||||
|
return self._shapes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tracks(self):
|
||||||
|
return self._tracks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
return self._version
|
||||||
|
|
||||||
|
@tags.setter
|
||||||
|
def tags(self, tags):
|
||||||
|
self._tags = tags
|
||||||
|
|
||||||
|
@shapes.setter
|
||||||
|
def shapes(self, shapes):
|
||||||
|
self._shapes = shapes
|
||||||
|
|
||||||
|
@tracks.setter
|
||||||
|
def tracks(self, tracks):
|
||||||
|
self._tracks = tracks
|
||||||
|
|
||||||
|
@version.setter
|
||||||
|
def version(self, version):
|
||||||
|
self._version = version
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return {
|
||||||
|
'version': self.version,
|
||||||
|
'tags': self.tags,
|
||||||
|
'shapes': self.shapes,
|
||||||
|
'tracks': self.tracks,
|
||||||
|
}
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
serializer = LabeledDataSerializer(data=self.data)
|
||||||
|
if serializer.is_valid(raise_exception=True):
|
||||||
|
return serializer.data
|
||||||
|
|
||||||
|
#makes a data copy from specified frame interval
|
||||||
|
def slice(self, start, stop):
|
||||||
|
is_frame_inside = lambda x: (start <= int(x['frame']) <= stop)
|
||||||
|
splitted_data = AnnotationIR()
|
||||||
|
splitted_data.tags = copy.deepcopy(list(filter(is_frame_inside, self.tags)))
|
||||||
|
splitted_data.shapes = copy.deepcopy(list(filter(is_frame_inside, self.shapes)))
|
||||||
|
splitted_data.tracks = copy.deepcopy(list(filter(lambda y: len(list(filter(is_frame_inside, y['shapes']))), self.tracks)))
|
||||||
|
|
||||||
|
return splitted_data
|
||||||
|
|
||||||
|
@data.setter
|
||||||
|
def data(self, data):
|
||||||
|
self.version = data['version']
|
||||||
|
self.tags = data['tags']
|
||||||
|
self.shapes = data['shapes']
|
||||||
|
self.tracks = data['tracks']
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._version = 0
|
||||||
|
self._tags = []
|
||||||
|
self._shapes = []
|
||||||
|
self._tracks = []
|
||||||
|
|
||||||
|
class Annotation:
|
||||||
|
Attribute = namedtuple('Attribute', 'name, value')
|
||||||
|
LabeledShape = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order')
|
||||||
|
LabeledShape.__new__.__defaults__ = (0, 0)
|
||||||
|
TrackedShape = namedtuple('TrackedShape', 'type, points, occluded, frame, attributes, outside, keyframe, z_order')
|
||||||
|
TrackedShape.__new__.__defaults__ = (0, )
|
||||||
|
Track = namedtuple('Track', 'label, group, shapes')
|
||||||
|
Tag = namedtuple('Tag', 'frame, label, attributes, group')
|
||||||
|
Tag.__new__.__defaults__ = (0, )
|
||||||
|
Frame = namedtuple('Frame', 'frame, name, width, height, labeled_shapes, tags')
|
||||||
|
|
||||||
|
def __init__(self, annotation_ir, db_task, scheme='', host='', create_callback=None):
|
||||||
|
self._annotation_ir = annotation_ir
|
||||||
|
self._db_task = db_task
|
||||||
|
self._scheme = scheme
|
||||||
|
self._host = host
|
||||||
|
self._create_callback=create_callback
|
||||||
|
self._MAX_ANNO_SIZE=30000
|
||||||
|
|
||||||
|
db_labels = self._db_task.label_set.all().prefetch_related('attributespec_set')
|
||||||
|
|
||||||
|
self._label_mapping = {db_label.id: db_label for db_label in db_labels}
|
||||||
|
|
||||||
|
self._attribute_mapping = {
|
||||||
|
'mutable': {},
|
||||||
|
'immutable': {},
|
||||||
|
}
|
||||||
|
for db_label in db_labels:
|
||||||
|
for db_attribute in db_label.attributespec_set.all():
|
||||||
|
if db_attribute.mutable:
|
||||||
|
self._attribute_mapping['mutable'][db_attribute.id] = db_attribute.name
|
||||||
|
else:
|
||||||
|
self._attribute_mapping['immutable'][db_attribute.id] = db_attribute.name
|
||||||
|
|
||||||
|
self._attribute_mapping_merged = {
|
||||||
|
**self._attribute_mapping['mutable'],
|
||||||
|
**self._attribute_mapping['immutable'],
|
||||||
|
}
|
||||||
|
|
||||||
|
self._init_frame_info()
|
||||||
|
self._init_meta()
|
||||||
|
|
||||||
|
def _get_label_id(self, label_name):
|
||||||
|
for db_label in self._label_mapping.values():
|
||||||
|
if label_name == db_label.name:
|
||||||
|
return db_label.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_label_name(self, label_id):
|
||||||
|
return self._label_mapping[label_id].name
|
||||||
|
|
||||||
|
def _get_attribute_name(self, attribute_id):
|
||||||
|
return self._attribute_mapping_merged[attribute_id]
|
||||||
|
|
||||||
|
def _get_attribute_id(self, attribute_name, attribute_type=None):
|
||||||
|
if attribute_type:
|
||||||
|
container = self._attribute_mapping[attribute_type]
|
||||||
|
else:
|
||||||
|
container = self._attribute_mapping_merged
|
||||||
|
|
||||||
|
for attr_id, attr_name in container.items():
|
||||||
|
if attribute_name == attr_name:
|
||||||
|
return attr_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_mutable_attribute_id(self, attribute_name):
|
||||||
|
return self._get_attribute_id(attribute_name, 'mutable')
|
||||||
|
|
||||||
|
def _get_immutable_attribute_id(self, attribute_name):
|
||||||
|
return self._get_attribute_id(attribute_name, 'immutable')
|
||||||
|
|
||||||
|
def _init_frame_info(self):
|
||||||
|
if self._db_task.mode == "interpolation":
|
||||||
|
self._frame_info = {
|
||||||
|
frame: {
|
||||||
|
"path": "frame_{:06d}".format(frame),
|
||||||
|
"width": self._db_task.video.width,
|
||||||
|
"height": self._db_task.video.height,
|
||||||
|
} for frame in range(self._db_task.size)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._frame_info = {db_image.frame: {
|
||||||
|
"path": db_image.path,
|
||||||
|
"width": db_image.width,
|
||||||
|
"height": db_image.height,
|
||||||
|
} for db_image in self._db_task.image_set.all()}
|
||||||
|
|
||||||
|
def _init_meta(self):
|
||||||
|
db_segments = self._db_task.segment_set.all().prefetch_related('job_set')
|
||||||
|
self._meta = OrderedDict([
|
||||||
|
("task", OrderedDict([
|
||||||
|
("id", str(self._db_task.id)),
|
||||||
|
("name", self._db_task.name),
|
||||||
|
("size", str(self._db_task.size)),
|
||||||
|
("mode", self._db_task.mode),
|
||||||
|
("overlap", str(self._db_task.overlap)),
|
||||||
|
("bugtracker", self._db_task.bug_tracker),
|
||||||
|
("created", str(timezone.localtime(self._db_task.created_date))),
|
||||||
|
("updated", str(timezone.localtime(self._db_task.updated_date))),
|
||||||
|
("start_frame", str(self._db_task.start_frame)),
|
||||||
|
("stop_frame", str(self._db_task.stop_frame)),
|
||||||
|
("frame_filter", self._db_task.frame_filter),
|
||||||
|
("z_order", str(self._db_task.z_order)),
|
||||||
|
|
||||||
|
("labels", [
|
||||||
|
("label", OrderedDict([
|
||||||
|
("name", db_label.name),
|
||||||
|
("attributes", [
|
||||||
|
("attribute", OrderedDict([
|
||||||
|
("name", db_attr.name),
|
||||||
|
("mutable", str(db_attr.mutable)),
|
||||||
|
("input_type", db_attr.input_type),
|
||||||
|
("default_value", db_attr.default_value),
|
||||||
|
("values", db_attr.values)]))
|
||||||
|
for db_attr in db_label.attributespec_set.all()])
|
||||||
|
])) for db_label in self._label_mapping.values()
|
||||||
|
]),
|
||||||
|
|
||||||
|
("segments", [
|
||||||
|
("segment", OrderedDict([
|
||||||
|
("id", str(db_segment.id)),
|
||||||
|
("start", str(db_segment.start_frame)),
|
||||||
|
("stop", str(db_segment.stop_frame)),
|
||||||
|
("url", "{0}://{1}/?id={2}".format(
|
||||||
|
self._scheme, self._host, db_segment.job_set.all()[0].id))]
|
||||||
|
)) for db_segment in db_segments
|
||||||
|
]),
|
||||||
|
|
||||||
|
("owner", OrderedDict([
|
||||||
|
("username", self._db_task.owner.username),
|
||||||
|
("email", self._db_task.owner.email)
|
||||||
|
]) if self._db_task.owner else ""),
|
||||||
|
|
||||||
|
("assignee", OrderedDict([
|
||||||
|
("username", self._db_task.assignee.username),
|
||||||
|
("email", self._db_task.assignee.email)
|
||||||
|
]) if self._db_task.assignee else ""),
|
||||||
|
])),
|
||||||
|
("dumped", str(timezone.localtime(timezone.now())))
|
||||||
|
])
|
||||||
|
|
||||||
|
if self._db_task.mode == "interpolation":
|
||||||
|
self._meta["task"]["original_size"] = OrderedDict([
|
||||||
|
("width", str(self._db_task.video.width)),
|
||||||
|
("height", str(self._db_task.video.height))
|
||||||
|
])
|
||||||
|
# Add source to dumped file
|
||||||
|
self._meta["source"] = str(os.path.basename(self._db_task.video.path))
|
||||||
|
|
||||||
|
def _export_attributes(self, attributes):
|
||||||
|
exported_attributes = []
|
||||||
|
for attr in attributes:
|
||||||
|
db_attribute = self._attribute_mapping_merged[attr["spec_id"]]
|
||||||
|
exported_attributes.append(Annotation.Attribute(
|
||||||
|
name=db_attribute,
|
||||||
|
value=attr["value"],
|
||||||
|
))
|
||||||
|
return exported_attributes
|
||||||
|
|
||||||
|
def _export_tracked_shape(self, shape):
|
||||||
|
return Annotation.TrackedShape(
|
||||||
|
type=shape["type"],
|
||||||
|
frame=self._db_task.start_frame + shape["frame"] * self._db_task.get_frame_step(),
|
||||||
|
points=shape["points"],
|
||||||
|
occluded=shape["occluded"],
|
||||||
|
outside=shape.get("outside", False),
|
||||||
|
keyframe=shape.get("keyframe", True),
|
||||||
|
z_order=shape["z_order"],
|
||||||
|
attributes=self._export_attributes(shape["attributes"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _export_labeled_shape(self, shape):
|
||||||
|
return Annotation.LabeledShape(
|
||||||
|
type=shape["type"],
|
||||||
|
label=self._get_label_name(shape["label_id"]),
|
||||||
|
frame=self._db_task.start_frame + shape["frame"] * self._db_task.get_frame_step(),
|
||||||
|
points=shape["points"],
|
||||||
|
occluded=shape["occluded"],
|
||||||
|
z_order=shape.get("z_order", 0),
|
||||||
|
group=shape.get("group", 0),
|
||||||
|
attributes=self._export_attributes(shape["attributes"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _export_tag(self, tag):
|
||||||
|
return Annotation.Tag(
|
||||||
|
frame=self._db_task.start_frame + tag["frame"] * self._db_task.get_frame_step(),
|
||||||
|
label=self._get_label_name(tag["label_id"]),
|
||||||
|
group=tag.get("group", 0),
|
||||||
|
attributes=self._export_attributes(tag["attributes"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def group_by_frame(self):
|
||||||
|
def _get_frame(annotations, shape):
|
||||||
|
db_image = self._frame_info[shape["frame"]]
|
||||||
|
frame = self._db_task.start_frame + shape["frame"] * self._db_task.get_frame_step()
|
||||||
|
rpath = db_image['path'].split(os.path.sep)
|
||||||
|
if len(rpath) != 1:
|
||||||
|
rpath = os.path.sep.join(rpath[rpath.index(".upload")+1:])
|
||||||
|
else:
|
||||||
|
rpath = rpath[0]
|
||||||
|
if frame not in annotations:
|
||||||
|
annotations[frame] = Annotation.Frame(
|
||||||
|
frame=frame,
|
||||||
|
name=rpath,
|
||||||
|
height=db_image["height"],
|
||||||
|
width=db_image["width"],
|
||||||
|
labeled_shapes=[],
|
||||||
|
tags=[],
|
||||||
|
)
|
||||||
|
return annotations[frame]
|
||||||
|
|
||||||
|
annotations = {}
|
||||||
|
data_manager = DataManager(self._annotation_ir)
|
||||||
|
for shape in data_manager.to_shapes(self._db_task.size):
|
||||||
|
_get_frame(annotations, shape).labeled_shapes.append(self._export_labeled_shape(shape))
|
||||||
|
|
||||||
|
for tag in self._annotation_ir.tags:
|
||||||
|
_get_frame(annotations, tag).tags.append(self._export_tag(tag))
|
||||||
|
|
||||||
|
return iter(annotations.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shapes(self):
|
||||||
|
for shape in self._annotation_ir.shapes:
|
||||||
|
yield self._export_labeled_shape(shape)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tracks(self):
|
||||||
|
for track in self._annotation_ir.tracks:
|
||||||
|
tracked_shapes = TrackManager.get_interpolated_shapes(track, 0, self._db_task.size)
|
||||||
|
yield Annotation.Track(
|
||||||
|
label=self._get_label_name(track["label_id"]),
|
||||||
|
group=track['group'],
|
||||||
|
shapes=[self._export_tracked_shape(shape) for shape in tracked_shapes],
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
for tag in self._annotation_ir.tags:
|
||||||
|
yield self._export_tag(tag)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meta(self):
|
||||||
|
return self._meta
|
||||||
|
|
||||||
|
def _import_tag(self, tag):
|
||||||
|
_tag = tag._asdict()
|
||||||
|
_tag['label_id'] = self._get_label_id(_tag.pop('label'))
|
||||||
|
_tag['attributes'] = [self._import_attribute(attrib) for attrib in _tag['attributes'] if self._get_attribute_id(attrib.name)]
|
||||||
|
return _tag
|
||||||
|
|
||||||
|
def _import_attribute(self, attribute):
|
||||||
|
return {
|
||||||
|
'spec_id': self._get_attribute_id(attribute.name),
|
||||||
|
'value': attribute.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _import_shape(self, shape):
|
||||||
|
_shape = shape._asdict()
|
||||||
|
_shape['label_id'] = self._get_label_id(_shape.pop('label'))
|
||||||
|
_shape['attributes'] = [self._import_attribute(attrib) for attrib in _shape['attributes'] if self._get_attribute_id(attrib.name)]
|
||||||
|
return _shape
|
||||||
|
|
||||||
|
def _import_track(self, track):
|
||||||
|
_track = track._asdict()
|
||||||
|
_track['frame'] = min(shape.frame for shape in _track['shapes'])
|
||||||
|
_track['label_id'] = self._get_label_id(_track.pop('label'))
|
||||||
|
_track['attributes'] = []
|
||||||
|
_track['shapes'] = [shape._asdict() for shape in _track['shapes']]
|
||||||
|
for shape in _track['shapes']:
|
||||||
|
_track['attributes'] = [self._import_attribute(attrib) for attrib in shape['attributes'] if self._get_immutable_attribute_id(attrib.name)]
|
||||||
|
shape['attributes'] = [self._import_attribute(attrib) for attrib in shape['attributes'] if self._get_mutable_attribute_id(attrib.name)]
|
||||||
|
|
||||||
|
return _track
|
||||||
|
|
||||||
|
def _call_callback(self):
|
||||||
|
if self._len() > self._MAX_ANNO_SIZE:
|
||||||
|
self._create_callback(self._annotation_ir.serialize())
|
||||||
|
self._annotation_ir.reset()
|
||||||
|
|
||||||
|
def add_tag(self, tag):
|
||||||
|
imported_tag = self._import_tag(tag)
|
||||||
|
if imported_tag['label_id']:
|
||||||
|
self._annotation_ir.add_tag(imported_tag)
|
||||||
|
self._call_callback()
|
||||||
|
|
||||||
|
def add_shape(self, shape):
|
||||||
|
imported_shape = self._import_shape(shape)
|
||||||
|
if imported_shape['label_id']:
|
||||||
|
self._annotation_ir.add_shape(imported_shape)
|
||||||
|
self._call_callback()
|
||||||
|
|
||||||
|
def add_track(self, track):
|
||||||
|
imported_track = self._import_track(track)
|
||||||
|
if imported_track['label_id']:
|
||||||
|
self._annotation_ir.add_track(imported_track)
|
||||||
|
self._call_callback()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._annotation_ir
|
||||||
|
|
||||||
|
def _len(self):
|
||||||
|
track_len = 0
|
||||||
|
for track in self._annotation_ir.tracks:
|
||||||
|
track_len += len(track['shapes'])
|
||||||
|
|
||||||
|
return len(self._annotation_ir.tags) + len(self._annotation_ir.shapes) + track_len
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.db.models.signals import post_migrate
|
||||||
|
from cvat.apps.annotation.settings import BUILTIN_FORMATS
|
||||||
|
|
||||||
|
def register_builtins_callback(sender, **kwargs):
|
||||||
|
from .format import register_format
|
||||||
|
for builtin_format in BUILTIN_FORMATS:
|
||||||
|
register_format(builtin_format)
|
||||||
|
|
||||||
|
class AnnotationConfig(AppConfig):
|
||||||
|
name = 'cvat.apps.annotation'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
post_migrate.connect(register_builtins_callback, sender=self)
|
||||||
@ -0,0 +1,407 @@
|
|||||||
|
format_spec = {
|
||||||
|
"name": "CVAT",
|
||||||
|
"dumpers": [
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version} for videos",
|
||||||
|
"format": "XML",
|
||||||
|
"version": "1.1",
|
||||||
|
"handler": "dump_as_cvat_interpolation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version} for images",
|
||||||
|
"format": "XML",
|
||||||
|
"version": "1.1",
|
||||||
|
"handler": "dump_as_cvat_annotation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"loaders": [
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version}",
|
||||||
|
"format": "XML",
|
||||||
|
"version": "1.1",
|
||||||
|
"handler": "load",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def pairwise(iterable):
|
||||||
|
a = iter(iterable)
|
||||||
|
return zip(a, a)
|
||||||
|
|
||||||
|
def create_xml_dumper(file_object):
|
||||||
|
from xml.sax.saxutils import XMLGenerator
|
||||||
|
from collections import OrderedDict
|
||||||
|
class XmlAnnotationWriter:
|
||||||
|
def __init__(self, file):
|
||||||
|
self.version = "1.1"
|
||||||
|
self.file = file
|
||||||
|
self.xmlgen = XMLGenerator(self.file, 'utf-8')
|
||||||
|
self._level = 0
|
||||||
|
|
||||||
|
def _indent(self, newline = True):
|
||||||
|
if newline:
|
||||||
|
self.xmlgen.ignorableWhitespace("\n")
|
||||||
|
self.xmlgen.ignorableWhitespace(" " * self._level)
|
||||||
|
|
||||||
|
def _add_version(self):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("version", {})
|
||||||
|
self.xmlgen.characters(self.version)
|
||||||
|
self.xmlgen.endElement("version")
|
||||||
|
|
||||||
|
def open_root(self):
|
||||||
|
self.xmlgen.startDocument()
|
||||||
|
self.xmlgen.startElement("annotations", {})
|
||||||
|
self._level += 1
|
||||||
|
self._add_version()
|
||||||
|
|
||||||
|
def _add_meta(self, meta):
|
||||||
|
self._level += 1
|
||||||
|
for k, v in meta.items():
|
||||||
|
if isinstance(v, OrderedDict):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement(k, {})
|
||||||
|
self._add_meta(v)
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement(k)
|
||||||
|
elif isinstance(v, list):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement(k, {})
|
||||||
|
for tup in v:
|
||||||
|
self._add_meta(OrderedDict([tup]))
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement(k)
|
||||||
|
else:
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement(k, {})
|
||||||
|
self.xmlgen.characters(v)
|
||||||
|
self.xmlgen.endElement(k)
|
||||||
|
self._level -= 1
|
||||||
|
|
||||||
|
def add_meta(self, meta):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("meta", {})
|
||||||
|
self._add_meta(meta)
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("meta")
|
||||||
|
|
||||||
|
def open_track(self, track):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("track", track)
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def open_image(self, image):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("image", image)
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def open_box(self, box):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("box", box)
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def open_polygon(self, polygon):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("polygon", polygon)
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def open_polyline(self, polyline):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("polyline", polyline)
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def open_points(self, points):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("points", points)
|
||||||
|
self._level += 1
|
||||||
|
|
||||||
|
def add_attribute(self, attribute):
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.startElement("attribute", {"name": attribute["name"]})
|
||||||
|
self.xmlgen.characters(attribute["value"])
|
||||||
|
self.xmlgen.endElement("attribute")
|
||||||
|
|
||||||
|
def close_box(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("box")
|
||||||
|
|
||||||
|
def close_polygon(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("polygon")
|
||||||
|
|
||||||
|
def close_polyline(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("polyline")
|
||||||
|
|
||||||
|
def close_points(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("points")
|
||||||
|
|
||||||
|
def close_image(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("image")
|
||||||
|
|
||||||
|
def close_track(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("track")
|
||||||
|
|
||||||
|
def close_root(self):
|
||||||
|
self._level -= 1
|
||||||
|
self._indent()
|
||||||
|
self.xmlgen.endElement("annotations")
|
||||||
|
self.xmlgen.endDocument()
|
||||||
|
|
||||||
|
return XmlAnnotationWriter(file_object)
|
||||||
|
|
||||||
|
def dump_as_cvat_annotation(file_object, annotations):
|
||||||
|
from collections import OrderedDict
|
||||||
|
dumper = create_xml_dumper(file_object)
|
||||||
|
dumper.open_root()
|
||||||
|
dumper.add_meta(annotations.meta)
|
||||||
|
|
||||||
|
for frame_annotation in annotations.group_by_frame():
|
||||||
|
frame_id = frame_annotation.frame
|
||||||
|
dumper.open_image(OrderedDict([
|
||||||
|
("id", str(frame_id)),
|
||||||
|
("name", frame_annotation.name),
|
||||||
|
("width", str(frame_annotation.width)),
|
||||||
|
("height", str(frame_annotation.height))
|
||||||
|
]))
|
||||||
|
|
||||||
|
for shape in frame_annotation.labeled_shapes:
|
||||||
|
dump_data = OrderedDict([
|
||||||
|
("label", shape.label),
|
||||||
|
("occluded", str(int(shape.occluded))),
|
||||||
|
])
|
||||||
|
|
||||||
|
if shape.type == "rectangle":
|
||||||
|
dump_data.update(OrderedDict([
|
||||||
|
("xtl", "{:.2f}".format(shape.points[0])),
|
||||||
|
("ytl", "{:.2f}".format(shape.points[1])),
|
||||||
|
("xbr", "{:.2f}".format(shape.points[2])),
|
||||||
|
("ybr", "{:.2f}".format(shape.points[3]))
|
||||||
|
]))
|
||||||
|
else:
|
||||||
|
dump_data.update(OrderedDict([
|
||||||
|
("points", ';'.join((
|
||||||
|
','.join((
|
||||||
|
"{:.2f}".format(x),
|
||||||
|
"{:.2f}".format(y)
|
||||||
|
)) for x, y in pairwise(shape.points))
|
||||||
|
)),
|
||||||
|
]))
|
||||||
|
|
||||||
|
if annotations.meta["task"]["z_order"] != "False":
|
||||||
|
dump_data['z_order'] = str(shape.z_order)
|
||||||
|
if "group" in shape and shape.group:
|
||||||
|
dump_data['group_id'] = str(shape.group)
|
||||||
|
|
||||||
|
if shape.type == "rectangle":
|
||||||
|
dumper.open_box(dump_data)
|
||||||
|
elif shape.type == "polygon":
|
||||||
|
dumper.open_polygon(dump_data)
|
||||||
|
elif shape.type == "polyline":
|
||||||
|
dumper.open_polyline(dump_data)
|
||||||
|
elif shape.type == "points":
|
||||||
|
dumper.open_points(dump_data)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("unknown shape type")
|
||||||
|
|
||||||
|
for attr in shape.attributes:
|
||||||
|
dumper.add_attribute(OrderedDict([
|
||||||
|
("name", attr.name),
|
||||||
|
("value", attr.value)
|
||||||
|
]))
|
||||||
|
|
||||||
|
if shape.type == "rectangle":
|
||||||
|
dumper.close_box()
|
||||||
|
elif shape.type == "polygon":
|
||||||
|
dumper.close_polygon()
|
||||||
|
elif shape.type == "polyline":
|
||||||
|
dumper.close_polyline()
|
||||||
|
elif shape.type == "points":
|
||||||
|
dumper.close_points()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("unknown shape type")
|
||||||
|
|
||||||
|
dumper.close_image()
|
||||||
|
dumper.close_root()
|
||||||
|
|
||||||
|
def dump_as_cvat_interpolation(file_object, annotations):
|
||||||
|
from collections import OrderedDict
|
||||||
|
dumper = create_xml_dumper(file_object)
|
||||||
|
dumper.open_root()
|
||||||
|
dumper.add_meta(annotations.meta)
|
||||||
|
def dump_track(idx, track):
|
||||||
|
track_id = idx
|
||||||
|
dump_data = OrderedDict([
|
||||||
|
("id", str(track_id)),
|
||||||
|
("label", track.label),
|
||||||
|
])
|
||||||
|
|
||||||
|
if track.group:
|
||||||
|
dump_data['group_id'] = str(track.group)
|
||||||
|
dumper.open_track(dump_data)
|
||||||
|
|
||||||
|
for shape in track.shapes:
|
||||||
|
dump_data = OrderedDict([
|
||||||
|
("frame", str(shape.frame)),
|
||||||
|
("outside", str(int(shape.outside))),
|
||||||
|
("occluded", str(int(shape.occluded))),
|
||||||
|
("keyframe", str(int(shape.keyframe))),
|
||||||
|
])
|
||||||
|
|
||||||
|
if shape.type == "rectangle":
|
||||||
|
dump_data.update(OrderedDict([
|
||||||
|
("xtl", "{:.2f}".format(shape.points[0])),
|
||||||
|
("ytl", "{:.2f}".format(shape.points[1])),
|
||||||
|
("xbr", "{:.2f}".format(shape.points[2])),
|
||||||
|
("ybr", "{:.2f}".format(shape.points[3])),
|
||||||
|
]))
|
||||||
|
else:
|
||||||
|
dump_data.update(OrderedDict([
|
||||||
|
("points", ';'.join(['{:.2f},{:.2f}'.format(x, y)
|
||||||
|
for x,y in pairwise(shape.points)]))
|
||||||
|
]))
|
||||||
|
|
||||||
|
if annotations.meta["task"]["z_order"] != "False":
|
||||||
|
dump_data["z_order"] = str(shape.z_order)
|
||||||
|
|
||||||
|
if shape.type == "rectangle":
|
||||||
|
dumper.open_box(dump_data)
|
||||||
|
elif shape.type == "polygon":
|
||||||
|
dumper.open_polygon(dump_data)
|
||||||
|
elif shape.type == "polyline":
|
||||||
|
dumper.open_polyline(dump_data)
|
||||||
|
elif shape.type == "points":
|
||||||
|
dumper.open_points(dump_data)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("unknown shape type")
|
||||||
|
|
||||||
|
for attr in shape.attributes:
|
||||||
|
dumper.add_attribute(OrderedDict([
|
||||||
|
("name", attr.name),
|
||||||
|
("value", attr.value)
|
||||||
|
]))
|
||||||
|
|
||||||
|
if shape.type == "rectangle":
|
||||||
|
dumper.close_box()
|
||||||
|
elif shape.type == "polygon":
|
||||||
|
dumper.close_polygon()
|
||||||
|
elif shape.type == "polyline":
|
||||||
|
dumper.close_polyline()
|
||||||
|
elif shape.type == "points":
|
||||||
|
dumper.close_points()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("unknown shape type")
|
||||||
|
dumper.close_track()
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
for track in annotations.tracks:
|
||||||
|
dump_track(counter, track)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
for shape in annotations.shapes:
|
||||||
|
dump_track(counter, annotations.Track(
|
||||||
|
label=shape.label,
|
||||||
|
group=shape.group,
|
||||||
|
shapes=[annotations.TrackedShape(
|
||||||
|
type=shape.type,
|
||||||
|
points=shape.points,
|
||||||
|
occluded=shape.occluded,
|
||||||
|
outside=False,
|
||||||
|
keyframe=True,
|
||||||
|
z_order=shape.z_order,
|
||||||
|
frame=shape.frame,
|
||||||
|
attributes=shape.attributes,
|
||||||
|
),
|
||||||
|
annotations.TrackedShape(
|
||||||
|
type=shape.type,
|
||||||
|
points=shape.points,
|
||||||
|
occluded=shape.occluded,
|
||||||
|
outside=True,
|
||||||
|
keyframe=True,
|
||||||
|
z_order=shape.z_order,
|
||||||
|
frame=shape.frame + 1,
|
||||||
|
attributes=shape.attributes,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
dumper.close_root()
|
||||||
|
|
||||||
|
def load(file_object, annotations):
|
||||||
|
import xml.etree.ElementTree as et
|
||||||
|
context = et.iterparse(file_object, events=("start", "end"))
|
||||||
|
context = iter(context)
|
||||||
|
ev, _ = next(context)
|
||||||
|
|
||||||
|
supported_shapes = ('box', 'polygon', 'polyline', 'points')
|
||||||
|
|
||||||
|
track = None
|
||||||
|
shape = None
|
||||||
|
image_is_opened = False
|
||||||
|
for ev, el in context:
|
||||||
|
if ev == 'start':
|
||||||
|
if el.tag == 'track':
|
||||||
|
track = annotations.Track(
|
||||||
|
label=el.attrib['label'],
|
||||||
|
group=int(el.attrib.get('group_id', 0)),
|
||||||
|
shapes=[],
|
||||||
|
)
|
||||||
|
elif el.tag == 'image':
|
||||||
|
image_is_opened = True
|
||||||
|
frame_id = int(el.attrib['id'])
|
||||||
|
elif el.tag in supported_shapes and (track is not None or image_is_opened):
|
||||||
|
shape = {
|
||||||
|
'attributes': [],
|
||||||
|
'points': [],
|
||||||
|
}
|
||||||
|
elif ev == 'end':
|
||||||
|
if el.tag == 'attribute' and shape is not None:
|
||||||
|
shape['attributes'].append(annotations.Attribute(
|
||||||
|
name=el.attrib['name'],
|
||||||
|
value=el.text,
|
||||||
|
))
|
||||||
|
if el.tag in supported_shapes:
|
||||||
|
if track is not None:
|
||||||
|
shape['frame'] = el.attrib['frame']
|
||||||
|
shape['outside'] = el.attrib['outside'] == "1"
|
||||||
|
shape['keyframe'] = el.attrib['keyframe'] == "1"
|
||||||
|
else:
|
||||||
|
shape['frame'] = frame_id
|
||||||
|
shape['label'] = el.attrib['label']
|
||||||
|
shape['group'] = int(el.attrib.get('group_id', 0))
|
||||||
|
|
||||||
|
shape['type'] = 'rectangle' if el.tag == 'box' else el.tag
|
||||||
|
shape['occluded'] = el.attrib['occluded'] == '1'
|
||||||
|
shape['z_order'] = int(el.attrib.get('z_order', 0))
|
||||||
|
|
||||||
|
if el.tag == 'box':
|
||||||
|
shape['points'].append(el.attrib['xtl'])
|
||||||
|
shape['points'].append(el.attrib['ytl'])
|
||||||
|
shape['points'].append(el.attrib['xbr'])
|
||||||
|
shape['points'].append(el.attrib['ybr'])
|
||||||
|
else:
|
||||||
|
for pair in el.attrib['points'].split(';'):
|
||||||
|
shape['points'].extend(map(float, pair.split(',')))
|
||||||
|
|
||||||
|
if track is not None:
|
||||||
|
track.shapes.append(annotations.TrackedShape(**shape))
|
||||||
|
else:
|
||||||
|
annotations.add_shape(annotations.LabeledShape(**shape))
|
||||||
|
shape = None
|
||||||
|
|
||||||
|
elif el.tag == 'track':
|
||||||
|
annotations.add_track(track)
|
||||||
|
track = None
|
||||||
|
elif el.tag == 'image':
|
||||||
|
image_is_opened = False
|
||||||
|
el.clear()
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
from cvat.apps.annotation import models
|
||||||
|
from django.conf import settings
|
||||||
|
from cvat.apps.annotation.serializers import AnnotationFormatSerializer
|
||||||
|
|
||||||
|
import os
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
def register_format(format_file):
|
||||||
|
source_code = open(format_file, 'r').read()
|
||||||
|
global_vars = {
|
||||||
|
"__builtins__": {},
|
||||||
|
}
|
||||||
|
exec(source_code, global_vars)
|
||||||
|
if "format_spec" not in global_vars or not isinstance(global_vars["format_spec"], dict):
|
||||||
|
raise Exception("Could not find \'format_spec\' definition in format file specification")
|
||||||
|
|
||||||
|
format_spec = deepcopy(global_vars["format_spec"])
|
||||||
|
|
||||||
|
if not models.AnnotationFormat.objects.filter(name=format_spec["name"]).exists():
|
||||||
|
format_spec["handler_file"] = os.path.relpath(format_file, settings.BASE_DIR)
|
||||||
|
for spec in format_spec["loaders"] + format_spec["dumpers"]:
|
||||||
|
spec["display_name"] = spec["display_name"].format(
|
||||||
|
name=format_spec["name"],
|
||||||
|
format=spec["format"],
|
||||||
|
version=spec["version"],
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = AnnotationFormatSerializer(data=format_spec)
|
||||||
|
if serializer.is_valid(raise_exception=True):
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
def get_annotation_formats():
|
||||||
|
return AnnotationFormatSerializer(
|
||||||
|
models.AnnotationFormat.objects.all(),
|
||||||
|
many=True).data
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 2.1.9 on 2019-07-31 15:20
|
||||||
|
|
||||||
|
import cvat.apps.annotation.models
|
||||||
|
import cvat.apps.engine.models
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.files.storage
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnnotationFormat',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', cvat.apps.engine.models.SafeCharField(max_length=256)),
|
||||||
|
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('handler_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(location=settings.BASE_DIR), upload_to=cvat.apps.annotation.models.upload_file_handler)),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'default_permissions': (),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnnotationHandler',
|
||||||
|
fields=[
|
||||||
|
('type', models.CharField(choices=[('dumper', 'DUMPER'), ('loader', 'LOADER')], max_length=16)),
|
||||||
|
('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)),
|
||||||
|
('format', models.CharField(max_length=16)),
|
||||||
|
('version', models.CharField(max_length=16)),
|
||||||
|
('handler', models.CharField(max_length=256)),
|
||||||
|
('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'default_permissions': (),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
from cvat.apps.engine.models import SafeCharField
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file_handler(instance, filename):
|
||||||
|
return os.path.join('formats', str(instance.id), filename)
|
||||||
|
|
||||||
|
class HandlerType(str, Enum):
|
||||||
|
DUMPER = 'dumper'
|
||||||
|
LOADER = 'loader'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def choices(self):
|
||||||
|
return tuple((x.value, x.name) for x in self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class AnnotationFormat(models.Model):
|
||||||
|
name = SafeCharField(max_length=256)
|
||||||
|
owner = models.ForeignKey(User, null=True, blank=True,
|
||||||
|
on_delete=models.SET_NULL)
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
handler_file = models.FileField(
|
||||||
|
upload_to=upload_file_handler,
|
||||||
|
storage=FileSystemStorage(location=os.path.join(settings.BASE_DIR)),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
class AnnotationHandler(models.Model):
|
||||||
|
type = models.CharField(max_length=16,
|
||||||
|
choices=HandlerType.choices())
|
||||||
|
display_name = SafeCharField(max_length=256, primary_key=True)
|
||||||
|
format = models.CharField(max_length=16)
|
||||||
|
version = models.CharField(max_length=16)
|
||||||
|
handler = models.CharField(max_length=256)
|
||||||
|
annotation_format = models.ForeignKey(AnnotationFormat, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
format_spec = {
|
||||||
|
"name": "PASCAL VOC",
|
||||||
|
"dumpers": [
|
||||||
|
{
|
||||||
|
"display_name": "{name} {format} {version}",
|
||||||
|
"format": "ZIP",
|
||||||
|
"version": "1.0",
|
||||||
|
"handler": "dump"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"loaders": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def load(file_object, annotations, spec):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def dump(file_object, annotations):
|
||||||
|
from pascal_voc_writer import Writer
|
||||||
|
import os
|
||||||
|
from zipfile import ZipFile
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
with TemporaryDirectory() as out_dir:
|
||||||
|
with ZipFile(file_object, 'w') as output_zip:
|
||||||
|
for frame_annotation in annotations.group_by_frame():
|
||||||
|
image_name = frame_annotation.name
|
||||||
|
width = frame_annotation.width
|
||||||
|
height = frame_annotation.height
|
||||||
|
|
||||||
|
writer = Writer(image_name, width, height)
|
||||||
|
writer.template_parameters['path'] = ''
|
||||||
|
writer.template_parameters['folder'] = ''
|
||||||
|
|
||||||
|
for shape in frame_annotation.labeled_shapes:
|
||||||
|
if shape.type != "rectangle":
|
||||||
|
continue
|
||||||
|
label = shape.label
|
||||||
|
xtl = shape.points[0]
|
||||||
|
ytl = shape.points[1]
|
||||||
|
xbr = shape.points[2]
|
||||||
|
ybr = shape.points[3]
|
||||||
|
writer.addObject(label, xtl, ytl, xbr, ybr)
|
||||||
|
|
||||||
|
anno_name = os.path.basename('{}.{}'.format(os.path.splitext(image_name)[0], 'xml'))
|
||||||
|
anno_file = os.path.join(out_dir, anno_name)
|
||||||
|
writer.save(anno_file)
|
||||||
|
output_zip.write(filename=anno_file, arcname=anno_name)
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
from cvat.apps.annotation import models
|
||||||
|
|
||||||
|
class AnnotationHandlerSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.AnnotationHandler
|
||||||
|
exclude = ('annotation_format',)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationFormatSerializer(serializers.ModelSerializer):
|
||||||
|
handlers = AnnotationHandlerSerializer(many=True, source='annotationhandler_set')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.AnnotationFormat
|
||||||
|
exclude = ("handler_file", )
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def create(self, validated_data):
|
||||||
|
handlers = validated_data.pop('handlers')
|
||||||
|
|
||||||
|
annotation_format = models.AnnotationFormat.objects.create(**validated_data)
|
||||||
|
|
||||||
|
handlers = [models.AnnotationHandler(annotation_format=annotation_format, **handler) for handler in handlers]
|
||||||
|
models.AnnotationHandler.objects.bulk_create(handlers)
|
||||||
|
|
||||||
|
return annotation_format
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
_data = data.copy()
|
||||||
|
_data["handlers"] = []
|
||||||
|
for d in _data.pop("dumpers"):
|
||||||
|
d["type"] = models.HandlerType.DUMPER
|
||||||
|
_data["handlers"].append(d)
|
||||||
|
|
||||||
|
for l in _data.pop("loaders"):
|
||||||
|
l["type"] = models.HandlerType.LOADER
|
||||||
|
_data["handlers"].append(l)
|
||||||
|
return _data
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
data['dumpers'] = []
|
||||||
|
data['loaders'] = []
|
||||||
|
for handler in data.pop("handlers"):
|
||||||
|
handler_type = handler.pop("type")
|
||||||
|
if handler_type == models.HandlerType.DUMPER:
|
||||||
|
data["dumpers"].append(handler)
|
||||||
|
else:
|
||||||
|
data["loaders"].append(handler)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
class AnnotationFileSerializer(serializers.Serializer):
|
||||||
|
annotation_file = serializers.FileField()
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
path_prefix = os.path.join('cvat', 'apps', 'annotation')
|
||||||
|
BUILTIN_FORMATS = (
|
||||||
|
os.path.join(path_prefix, 'cvat.py'),
|
||||||
|
os.path.join(path_prefix,'pascal_voc.py'),
|
||||||
|
)
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Copyright (C) 2018 Intel Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,385 @@
|
|||||||
|
import copy
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.optimize import linear_sum_assignment
|
||||||
|
from shapely import geometry
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class DataManager:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def merge(self, data, start_frame, overlap):
|
||||||
|
tags = TagManager(self.data.tags)
|
||||||
|
tags.merge(data.tags, start_frame, overlap)
|
||||||
|
|
||||||
|
shapes = ShapeManager(self.data.shapes)
|
||||||
|
shapes.merge(data.shapes, start_frame, overlap)
|
||||||
|
|
||||||
|
tracks = TrackManager(self.data.tracks)
|
||||||
|
tracks.merge(data.tracks, start_frame, overlap)
|
||||||
|
|
||||||
|
def to_shapes(self, end_frame):
|
||||||
|
shapes = self.data.shapes
|
||||||
|
tracks = TrackManager(self.data.tracks)
|
||||||
|
|
||||||
|
return shapes + tracks.to_shapes(end_frame)
|
||||||
|
|
||||||
|
def to_tracks(self):
|
||||||
|
tracks = self.data.tracks
|
||||||
|
shapes = ShapeManager(self.data.shapes)
|
||||||
|
|
||||||
|
return tracks + shapes.to_tracks()
|
||||||
|
|
||||||
|
class ObjectManager:
|
||||||
|
def __init__(self, objects):
|
||||||
|
self.objects = objects
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_objects_by_frame(objects, start_frame):
|
||||||
|
objects_by_frame = {}
|
||||||
|
for obj in objects:
|
||||||
|
if obj["frame"] >= start_frame:
|
||||||
|
if obj["frame"] in objects_by_frame:
|
||||||
|
objects_by_frame[obj["frame"]].append(obj)
|
||||||
|
else:
|
||||||
|
objects_by_frame[obj["frame"]] = [obj]
|
||||||
|
|
||||||
|
return objects_by_frame
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_cost_threshold():
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unite_objects(obj0, obj1):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _modify_unmached_object(obj, end_frame):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def merge(self, objects, start_frame, overlap):
|
||||||
|
# 1. Split objects on two parts: new and which can be intersected
|
||||||
|
# with existing objects.
|
||||||
|
new_objects = [obj for obj in objects
|
||||||
|
if obj["frame"] >= start_frame + overlap]
|
||||||
|
int_objects = [obj for obj in objects
|
||||||
|
if obj["frame"] < start_frame + overlap]
|
||||||
|
assert len(new_objects) + len(int_objects) == len(objects)
|
||||||
|
|
||||||
|
# 2. Convert to more convenient data structure (objects by frame)
|
||||||
|
int_objects_by_frame = self._get_objects_by_frame(int_objects, start_frame)
|
||||||
|
old_objects_by_frame = self._get_objects_by_frame(self.objects, start_frame)
|
||||||
|
|
||||||
|
# 3. Add new objects as is. It should be done only after old_objects_by_frame
|
||||||
|
# variable is initialized.
|
||||||
|
self.objects.extend(new_objects)
|
||||||
|
|
||||||
|
# Nothing to merge here. Just add all int_objects if any.
|
||||||
|
if not old_objects_by_frame or not int_objects_by_frame:
|
||||||
|
for frame in old_objects_by_frame:
|
||||||
|
for old_obj in old_objects_by_frame[frame]:
|
||||||
|
self._modify_unmached_object(old_obj, start_frame + overlap)
|
||||||
|
self.objects.extend(int_objects)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Build cost matrix for each frame and find correspondence using
|
||||||
|
# Hungarian algorithm. In this case min_cost_thresh is stronger
|
||||||
|
# because we compare only on one frame.
|
||||||
|
min_cost_thresh = self._get_cost_threshold()
|
||||||
|
for frame in int_objects_by_frame:
|
||||||
|
if frame in old_objects_by_frame:
|
||||||
|
int_objects = int_objects_by_frame[frame]
|
||||||
|
old_objects = old_objects_by_frame[frame]
|
||||||
|
cost_matrix = np.empty(shape=(len(int_objects), len(old_objects)),
|
||||||
|
dtype=float)
|
||||||
|
# 5.1 Construct cost matrix for the frame.
|
||||||
|
for i, int_obj in enumerate(int_objects):
|
||||||
|
for j, old_obj in enumerate(old_objects):
|
||||||
|
cost_matrix[i][j] = 1 - self._calc_objects_similarity(
|
||||||
|
int_obj, old_obj, start_frame, overlap)
|
||||||
|
|
||||||
|
# 6. Find optimal solution using Hungarian algorithm.
|
||||||
|
row_ind, col_ind = linear_sum_assignment(cost_matrix)
|
||||||
|
old_objects_indexes = list(range(0, len(old_objects)))
|
||||||
|
int_objects_indexes = list(range(0, len(int_objects)))
|
||||||
|
for i, j in zip(row_ind, col_ind):
|
||||||
|
# Reject the solution if the cost is too high. Remember
|
||||||
|
# inside int_objects_indexes objects which were handled.
|
||||||
|
if cost_matrix[i][j] <= min_cost_thresh:
|
||||||
|
old_objects[j] = self._unite_objects(int_objects[i], old_objects[j])
|
||||||
|
int_objects_indexes[i] = -1
|
||||||
|
old_objects_indexes[j] = -1
|
||||||
|
|
||||||
|
# 7. Add all new objects which were not processed.
|
||||||
|
for i in int_objects_indexes:
|
||||||
|
if i != -1:
|
||||||
|
self.objects.append(int_objects[i])
|
||||||
|
|
||||||
|
# 8. Modify all old objects which were not processed
|
||||||
|
# (e.g. generate a shape with outside=True at the end).
|
||||||
|
for j in old_objects_indexes:
|
||||||
|
if j != -1:
|
||||||
|
self._modify_unmached_object(old_objects[j],
|
||||||
|
start_frame + overlap)
|
||||||
|
else:
|
||||||
|
# We don't have old objects on the frame. Let's add all new ones.
|
||||||
|
self.objects.extend(int_objects_by_frame[frame])
|
||||||
|
|
||||||
|
class TagManager(ObjectManager):
|
||||||
|
@staticmethod
|
||||||
|
def _get_cost_threshold():
|
||||||
|
return 0.25
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
||||||
|
# TODO: improve the trivial implementation, compare attributes
|
||||||
|
return 1 if obj0["label_id"] == obj1["label_id"] else 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unite_objects(obj0, obj1):
|
||||||
|
# TODO: improve the trivial implementation
|
||||||
|
return obj0 if obj0["frame"] < obj1["frame"] else obj1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _modify_unmached_object(obj, end_frame):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pairwise(iterable):
|
||||||
|
a = iter(iterable)
|
||||||
|
return zip(a, a)
|
||||||
|
|
||||||
|
class ShapeManager(ObjectManager):
|
||||||
|
def to_tracks(self):
|
||||||
|
tracks = []
|
||||||
|
for shape in self.objects:
|
||||||
|
shape0 = copy.copy(shape)
|
||||||
|
shape0["keyframe"] = True
|
||||||
|
shape0["outside"] = False
|
||||||
|
# TODO: Separate attributes on mutable and unmutable
|
||||||
|
shape0["attributes"] = []
|
||||||
|
shape0.pop("group", None)
|
||||||
|
shape1 = copy.copy(shape0)
|
||||||
|
shape1["outside"] = True
|
||||||
|
shape1["frame"] += 1
|
||||||
|
|
||||||
|
track = {
|
||||||
|
"label_id": shape["label_id"],
|
||||||
|
"frame": shape["frame"],
|
||||||
|
"group": shape.get("group", None),
|
||||||
|
"attributes": shape["attributes"],
|
||||||
|
"shapes": [shape0, shape1]
|
||||||
|
}
|
||||||
|
tracks.append(track)
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_cost_threshold():
|
||||||
|
return 0.25
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
||||||
|
def _calc_polygons_similarity(p0, p1):
|
||||||
|
overlap_area = p0.intersection(p1).area
|
||||||
|
return overlap_area / (p0.area + p1.area - overlap_area)
|
||||||
|
|
||||||
|
has_same_type = obj0["type"] == obj1["type"]
|
||||||
|
has_same_label = obj0.get("label_id") == obj1.get("label_id")
|
||||||
|
if has_same_type and has_same_label:
|
||||||
|
if obj0["type"] == models.ShapeType.RECTANGLE:
|
||||||
|
p0 = geometry.box(*obj0["points"])
|
||||||
|
p1 = geometry.box(*obj1["points"])
|
||||||
|
|
||||||
|
return _calc_polygons_similarity(p0, p1)
|
||||||
|
elif obj0["type"] == models.ShapeType.POLYGON:
|
||||||
|
p0 = geometry.Polygon(pairwise(obj0["points"]))
|
||||||
|
p1 = geometry.Polygon(pairwise(obj0["points"]))
|
||||||
|
|
||||||
|
return _calc_polygons_similarity(p0, p1)
|
||||||
|
else:
|
||||||
|
return 0 # FIXME: need some similarity for points and polylines
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unite_objects(obj0, obj1):
|
||||||
|
# TODO: improve the trivial implementation
|
||||||
|
return obj0 if obj0["frame"] < obj1["frame"] else obj1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _modify_unmached_object(obj, end_frame):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TrackManager(ObjectManager):
|
||||||
|
def to_shapes(self, end_frame):
|
||||||
|
shapes = []
|
||||||
|
for idx, track in enumerate(self.objects):
|
||||||
|
for shape in TrackManager.get_interpolated_shapes(track, 0, end_frame):
|
||||||
|
if not shape["outside"]:
|
||||||
|
shape["label_id"] = track["label_id"]
|
||||||
|
shape["group"] = track["group"]
|
||||||
|
shape["track_id"] = idx
|
||||||
|
shape["attributes"] += track["attributes"]
|
||||||
|
shapes.append(shape)
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_objects_by_frame(objects, start_frame):
|
||||||
|
# Just for unification. All tracks are assigned on the same frame
|
||||||
|
objects_by_frame = {0: []}
|
||||||
|
for obj in objects:
|
||||||
|
shape = obj["shapes"][-1] # optimization for old tracks
|
||||||
|
if shape["frame"] >= start_frame or not shape["outside"]:
|
||||||
|
objects_by_frame[0].append(obj)
|
||||||
|
|
||||||
|
if not objects_by_frame[0]:
|
||||||
|
objects_by_frame = {}
|
||||||
|
|
||||||
|
return objects_by_frame
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_cost_threshold():
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
||||||
|
if obj0["label_id"] == obj1["label_id"]:
|
||||||
|
# Here start_frame is the start frame of next segment
|
||||||
|
# and stop_frame is the stop frame of current segment
|
||||||
|
# end_frame == stop_frame + 1
|
||||||
|
end_frame = start_frame + overlap
|
||||||
|
obj0_shapes = TrackManager.get_interpolated_shapes(obj0, start_frame, end_frame)
|
||||||
|
obj1_shapes = TrackManager.get_interpolated_shapes(obj1, start_frame, end_frame)
|
||||||
|
obj0_shapes_by_frame = {shape["frame"]:shape for shape in obj0_shapes}
|
||||||
|
obj1_shapes_by_frame = {shape["frame"]:shape for shape in obj1_shapes}
|
||||||
|
assert obj0_shapes_by_frame and obj1_shapes_by_frame
|
||||||
|
|
||||||
|
count, error = 0, 0
|
||||||
|
for frame in range(start_frame, end_frame):
|
||||||
|
shape0 = obj0_shapes_by_frame.get(frame)
|
||||||
|
shape1 = obj1_shapes_by_frame.get(frame)
|
||||||
|
if shape0 and shape1:
|
||||||
|
if shape0["outside"] != shape1["outside"]:
|
||||||
|
error += 1
|
||||||
|
else:
|
||||||
|
error += 1 - ShapeManager._calc_objects_similarity(shape0, shape1, start_frame, overlap)
|
||||||
|
count += 1
|
||||||
|
elif shape0 or shape1:
|
||||||
|
error += 1
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return 1 - error / count
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _modify_unmached_object(obj, end_frame):
|
||||||
|
shape = obj["shapes"][-1]
|
||||||
|
if not shape["outside"]:
|
||||||
|
shape = copy.deepcopy(shape)
|
||||||
|
shape["frame"] = end_frame
|
||||||
|
shape["outside"] = True
|
||||||
|
obj["shapes"].append(shape)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalize_shape(shape):
|
||||||
|
points = np.asarray(shape["points"]).reshape(-1, 2)
|
||||||
|
broken_line = geometry.LineString(points)
|
||||||
|
points = []
|
||||||
|
for off in range(0, 100, 1):
|
||||||
|
p = broken_line.interpolate(off / 100, True)
|
||||||
|
points.append(p.x)
|
||||||
|
points.append(p.y)
|
||||||
|
|
||||||
|
shape = copy.copy(shape)
|
||||||
|
shape["points"] = points
|
||||||
|
|
||||||
|
return shape
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_interpolated_shapes(track, start_frame, end_frame):
|
||||||
|
def interpolate(shape0, shape1):
|
||||||
|
shapes = []
|
||||||
|
is_same_type = shape0["type"] == shape1["type"]
|
||||||
|
is_polygon = shape0["type"] == models.ShapeType.POLYGON
|
||||||
|
is_polyline = shape0["type"] == models.ShapeType.POLYLINE
|
||||||
|
is_same_size = len(shape0["points"]) == len(shape1["points"])
|
||||||
|
if not is_same_type or is_polygon or is_polyline or not is_same_size:
|
||||||
|
shape0 = TrackManager.normalize_shape(shape0)
|
||||||
|
shape1 = TrackManager.normalize_shape(shape1)
|
||||||
|
|
||||||
|
distance = shape1["frame"] - shape0["frame"]
|
||||||
|
step = np.subtract(shape1["points"], shape0["points"]) / distance
|
||||||
|
for frame in range(shape0["frame"] + 1, shape1["frame"]):
|
||||||
|
off = frame - shape0["frame"]
|
||||||
|
if shape1["outside"]:
|
||||||
|
points = np.asarray(shape0["points"]).reshape(-1, 2)
|
||||||
|
else:
|
||||||
|
points = (shape0["points"] + step * off).reshape(-1, 2)
|
||||||
|
shape = copy.deepcopy(shape0)
|
||||||
|
if len(points) == 1:
|
||||||
|
shape["points"] = points.flatten()
|
||||||
|
else:
|
||||||
|
broken_line = geometry.LineString(points).simplify(0.05, False)
|
||||||
|
shape["points"] = [x for p in broken_line.coords for x in p]
|
||||||
|
|
||||||
|
shape["keyframe"] = False
|
||||||
|
shape["frame"] = frame
|
||||||
|
shapes.append(shape)
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
if track.get("interpolated_shapes"):
|
||||||
|
return track["interpolated_shapes"]
|
||||||
|
|
||||||
|
# TODO: should be return an iterator?
|
||||||
|
shapes = []
|
||||||
|
curr_frame = track["shapes"][0]["frame"]
|
||||||
|
prev_shape = {}
|
||||||
|
for shape in track["shapes"]:
|
||||||
|
if prev_shape:
|
||||||
|
assert shape["frame"] > curr_frame
|
||||||
|
for attr in prev_shape["attributes"]:
|
||||||
|
if attr["spec_id"] not in map(lambda el: el["spec_id"], shape["attributes"]):
|
||||||
|
shape["attributes"].append(copy.deepcopy(attr))
|
||||||
|
if not prev_shape["outside"]:
|
||||||
|
shapes.extend(interpolate(prev_shape, shape))
|
||||||
|
|
||||||
|
shape["keyframe"] = True
|
||||||
|
shapes.append(shape)
|
||||||
|
curr_frame = shape["frame"]
|
||||||
|
prev_shape = shape
|
||||||
|
|
||||||
|
# TODO: Need to modify a client and a database (append "outside" shapes for polytracks)
|
||||||
|
if not prev_shape["outside"] and prev_shape["type"] == models.ShapeType.RECTANGLE:
|
||||||
|
shape = copy.copy(prev_shape)
|
||||||
|
shape["frame"] = end_frame
|
||||||
|
shapes.extend(interpolate(prev_shape, shape))
|
||||||
|
|
||||||
|
track["interpolated_shapes"] = shapes
|
||||||
|
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unite_objects(obj0, obj1):
|
||||||
|
track = obj0 if obj0["frame"] < obj1["frame"] else obj1
|
||||||
|
assert obj0["label_id"] == obj1["label_id"]
|
||||||
|
shapes = {shape["frame"]:shape for shape in obj0["shapes"]}
|
||||||
|
for shape in obj1["shapes"]:
|
||||||
|
frame = shape["frame"]
|
||||||
|
if frame in shapes:
|
||||||
|
shapes[frame] = ShapeManager._unite_objects(shapes[frame], shape)
|
||||||
|
else:
|
||||||
|
shapes[frame] = shape
|
||||||
|
|
||||||
|
track["frame"] = min(obj0["frame"], obj1["frame"])
|
||||||
|
track["shapes"] = list(sorted(shapes.values(), key=lambda shape: shape["frame"]))
|
||||||
|
track["interpolated_shapes"] = []
|
||||||
|
|
||||||
|
return track
|
||||||
Loading…
Reference in New Issue