You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2129 lines
90 KiB
Python

# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os
import copy
from django.utils import timezone
from collections import OrderedDict
import numpy as np
from scipy.optimize import linear_sum_assignment
from collections import OrderedDict
from distutils.util import strtobool
from xml.sax.saxutils import XMLGenerator
from abc import ABCMeta, abstractmethod
from PIL import Image
import django_rq
from django.conf import settings
from django.db import transaction
from . import models
from .task import get_frame_path, get_image_meta_cache
from .log import slogger
############################# Low Level server API
FORMAT_XML = 1
FORMAT_JSON = 2
def dump(tid, data_format, scheme, host):
"""
Dump annotation for the task in specified data format.
"""
queue = django_rq.get_queue('default')
queue.enqueue_call(func=_dump, args=(tid, data_format, scheme, host),
job_id="annotation.dump/{}".format(tid))
def check(tid):
"""
Check that potentially long operation 'dump' is completed.
Return the status as json/dictionary object.
"""
queue = django_rq.get_queue('default')
job = queue.fetch_job("annotation.dump/{}".format(tid))
if job is None:
response = {"state": "unknown"}
elif job.is_failed:
# FIXME: here we have potential race. In general job.exc_info is
# initialized inside handler but the method can be called before
# that. By a reason exc_info isn't initialized by RQ python.
response = {
"state": "error",
"stderr": job.exc_info}
elif job.is_finished:
response = {"state": "created"}
else:
response = {"state": "started"}
return response
@transaction.atomic
def get(jid):
"""
Get annotations for the job.
"""
db_job = models.Job.objects.select_for_update().get(id=jid)
annotation = _AnnotationForJob(db_job)
annotation.init_from_db()
return annotation.to_client()
@transaction.atomic
def save_job(jid, data, delete_old_data=False):
"""
Save new annotations for the job.
"""
db_job = models.Job.objects.select_for_update().get(id=jid)
annotation = _AnnotationForJob(db_job)
if delete_old_data:
annotation.delete_all_shapes_from_db()
annotation.validate_data_from_client(data)
annotation.delete_from_db(data['delete'])
annotation.save_to_db(data['create'])
annotation.update_in_db(data['update'])
db_job.segment.task.updated_date = timezone.now()
db_job.segment.task.save()
# pylint: disable=unused-argument
def save_task(tid, data):
"""
Save new annotations for the task.
"""
db_task = models.Task.objects.get(id=tid)
db_segments = list(db_task.segment_set.prefetch_related('job_set').all())
splitted_data = {}
for segment in db_segments:
jid = segment.job_set.first().id
start = segment.start_frame
stop = segment.stop_frame
splitted_data[jid] = {}
for action in ['create', 'update', 'delete']:
splitted_data[jid][action] = {
"boxes": list(filter(lambda x: start <= int(x['frame']) <= stop, data[action]['boxes'])),
"polygons": list(filter(lambda x: start <= int(x['frame']) <= stop, data[action]['polygons'])),
"polylines": list(filter(lambda x: start <= int(x['frame']) <= stop, data[action]['polylines'])),
"points": list(filter(lambda x: start <= int(x['frame']) <= stop, data[action]['points'])),
"box_paths": list(filter(lambda x: len(list(filter(lambda y: (start <= int(y['frame']) <= stop) and (not y['outside']), x['shapes']))), data[action]['box_paths'])),
"polygon_paths": list(filter(lambda x: len(list(filter(lambda y: (start <= int(y['frame']) <= stop) and (not y['outside']), x['shapes']))), data[action]['polygon_paths'])),
"polyline_paths": list(filter(lambda x: len(list(filter(lambda y: (start <= int(y['frame']) <= stop) and (not y['outside']), x['shapes']))), data[action]['polyline_paths'])),
"points_paths": list(filter(lambda x: len(list(filter(lambda y: (start <= int(y['frame']) <= stop) and (not y['outside']), x['shapes']))), data[action]['points_paths'])),
}
for jid, _data in splitted_data.items():
save_job(jid, _data, True)
# pylint: disable=unused-argument
def rq_handler(job, exc_type, exc_value, traceback):
tid = job.id.split('/')[1]
slogger.task[tid].error("dump annotation error was occured", exc_info=True)
##################################################
class _Label:
def __init__(self, db_label):
self.id = db_label.id
self.name = db_label.name
class _Attribute:
def __init__(self, db_attr, value):
self.id = db_attr.id
self.name = db_attr.get_name()
if db_attr.get_type() == 'checkbox':
self.value = str(value).lower()
else:
self.value = str(value)
class _BoundingBox:
def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, client_id=None, attributes=None):
self.xtl = x0
self.ytl = y0
self.xbr = x1
self.ybr = y1
self.occluded = occluded
self.z_order = z_order
self.client_id = client_id
self.frame = frame
self.attributes = attributes if attributes else []
def merge(self, box):
# The occluded property and attributes cannot be merged. Let's keep
# original attributes and occluded property of the self object.
assert self.frame == box.frame
self.xtl = (self.xtl + box.xtl) / 2
self.ytl = (self.ytl + box.ytl) / 2
self.xbr = (self.xbr + box.xbr) / 2
self.ybr = (self.ybr + box.ybr) / 2
def add_attribute(self, attr):
self.attributes.append(attr)
class _LabeledBox(_BoundingBox):
def __init__(self, label, x0, y0, x1, y1, frame, group_id, occluded, z_order, client_id=None, attributes=None):
super().__init__(x0, y0, x1, y1, frame, occluded, z_order, client_id, attributes)
self.label = label
self.group_id = group_id
class _TrackedBox(_BoundingBox):
def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, outside, attributes=None):
super().__init__(x0, y0, x1, y1, frame, occluded, z_order, None, attributes)
self.outside = outside
class _InterpolatedBox(_TrackedBox):
def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, outside, keyframe, attributes=None):
super().__init__(x0, y0, x1, y1, frame, occluded, z_order, outside, attributes)
self.keyframe = keyframe
class _PolyShape:
def __init__(self, points, frame, occluded, z_order, client_id=None, attributes=None):
self.points = points
self.frame = frame
self.occluded = occluded
self.z_order = z_order
self.client_id=client_id
self.attributes = attributes if attributes else []
def add_attribute(self, attr):
self.attributes.append(attr)
class _LabeledPolyShape(_PolyShape):
def __init__(self, label, points, frame, group_id, occluded, z_order, client_id=None, attributes=None):
super().__init__(points, frame, occluded, z_order, client_id, attributes)
self.label = label
self.group_id = group_id
class _TrackedPolyShape(_PolyShape):
def __init__(self, points, frame, occluded, z_order, outside, attributes=None):
super().__init__(points, frame, occluded, z_order, None, attributes)
self.outside = outside
class _InterpolatedPolyShape(_TrackedPolyShape):
def __init__(self, points, frame, occluded, z_order, outside, keyframe, attributes=None):
super().__init__(points, frame, occluded, z_order, outside, attributes)
self.keyframe = keyframe
class _BoxPath:
def __init__(self, label, start_frame, stop_frame, group_id, boxes=None, client_id=None, attributes=None):
self.label = label
self.frame = start_frame
self.stop_frame = stop_frame
self.group_id = group_id
self.boxes = boxes if boxes else []
self.client_id = client_id
self.attributes = attributes if attributes else []
self._interpolated_boxes = []
assert not self.boxes or self.boxes[-1].frame <= self.stop_frame
def add_box(self, box):
self.boxes.append(box)
def get_interpolated_boxes(self):
if not self._interpolated_boxes:
self._init_interpolated_boxes()
return self._interpolated_boxes
def _init_interpolated_boxes(self):
assert self.boxes[-1].frame <= self.stop_frame
boxes = []
stop_box = copy.copy(self.boxes[-1])
stop_box.frame = self.stop_frame + 1
attributes = {}
for box0, box1 in zip(self.boxes, self.boxes[1:] + [stop_box]):
assert box0.frame < box1.frame
distance = float(box1.frame - box0.frame)
delta_xtl = (box1.xtl - box0.xtl) / distance
delta_ytl = (box1.ytl - box0.ytl) / distance
delta_xbr = (box1.xbr - box0.xbr) / distance
delta_ybr = (box1.ybr - box0.ybr) / distance
# New box doesn't have all attributes (only first one does).
# Thus it is necessary to propagate them.
for attr in box0.attributes:
attributes[attr.id] = attr
for frame in range(box0.frame, box1.frame):
off = frame - box0.frame
xtl = box0.xtl + delta_xtl * off
ytl = box0.ytl + delta_ytl * off
xbr = box0.xbr + delta_xbr * off
ybr = box0.ybr + delta_ybr * off
box = _InterpolatedBox(xtl, ytl, xbr, ybr, frame, box0.occluded, box0.z_order,
box0.outside, box0.frame == frame, list(attributes.values()))
boxes.append(box)
if box0.outside:
break
self._interpolated_boxes = boxes
def merge(self, path):
assert self.label.id == path.label.id
boxes = {box.frame:box for box in self.boxes}
for box in path.boxes:
if box.frame in boxes:
boxes[box.frame].merge(box)
else:
boxes[box.frame] = box
self.frame = min(self.frame, path.frame)
self.stop_frame = max(self.stop_frame, path.stop_frame)
self.boxes = list(sorted(boxes.values(), key=lambda box: box.frame))
self._interpolated_boxes = []
def add_attribute(self, attr):
self.attributes.append(attr)
class _PolyPath:
def __init__(self, label, start_frame, stop_frame, group_id, shapes=None, client_id=None, attributes=None):
self.label = label
self.frame = start_frame
self.stop_frame = stop_frame
self.group_id = group_id
self.shapes = shapes if shapes else []
self.client_id = client_id
self.attributes = attributes if attributes else []
self._interpolated_shapes = [] # ???
def add_shape(self, shape):
self.shapes.append(shape)
def get_interpolated_shapes(self):
if not self._interpolated_shapes:
self._init_interpolated_shapes()
return self._interpolated_shapes
def _init_interpolated_shapes(self):
assert self.shapes[-1].frame <= self.stop_frame
self._interpolated_shapes = []
shapes = {shape.frame: shape for shape in self.shapes}
outside = False
attributes = {}
for frame in range(self.frame, self.stop_frame + 1):
if frame in shapes:
for attr in shapes[frame].attributes:
attributes[attr.id] = attr
shape = _InterpolatedPolyShape(shapes[frame].points, frame,
shapes[frame].occluded, shapes[frame].z_order, shapes[frame].outside, True, list(attributes.values()))
outside = shape.outside
self._interpolated_shapes.append(shape)
elif not outside:
shape = _InterpolatedPolyShape(self._interpolated_shapes[-1].points, frame, False,
0, True, True, list(attributes.values()))
outside = shape.outside
self._interpolated_shapes.append(shape)
def merge(self, path):
pass
def add_attribute(self, attr):
self.attributes.append(attr)
class _Annotation:
def __init__(self, start_frame, stop_frame):
self.start_frame = start_frame
self.stop_frame = stop_frame
self.reset()
def reset(self):
self.boxes = []
self.box_paths = []
self.polygons = []
self.polygon_paths = []
self.polylines = []
self.polyline_paths = []
self.points = []
self.points_paths = []
def get_max_client_id(self):
max_client_id = -1
def extract_client_id(shape):
return shape.client_id
if self.boxes:
max_client_id = max(max_client_id, (max(self.boxes, key=extract_client_id)).client_id)
if self.box_paths:
max_client_id = max(max_client_id, (max(self.box_paths, key=extract_client_id)).client_id)
if self.polygons:
max_client_id = max(max_client_id, (max(self.polygons, key=extract_client_id)).client_id)
if self.polygon_paths:
max_client_id = max(max_client_id, (max(self.polygon_paths, key=extract_client_id)).client_id)
if self.polylines:
max_client_id = max(max_client_id, (max(self.polylines, key=extract_client_id)).client_id)
if self.polyline_paths:
max_client_id = max(max_client_id, (max(self.polyline_paths, key=extract_client_id)).client_id)
if self.points:
max_client_id = max(max_client_id, (max(self.points, key=extract_client_id)).client_id)
if self.points_paths:
max_client_id = max(max_client_id, (max(self.points_paths, key=extract_client_id)).client_id)
return max_client_id
# Functions below used by dump functionality
def to_boxes(self, start_client_id):
boxes = []
for path in self.box_paths:
for box in path.get_interpolated_boxes():
if not box.outside:
box = _LabeledBox(
label=path.label,
x0=box.xtl, y0=box.ytl, x1=box.xbr, y1=box.ybr,
frame=box.frame,
group_id=path.group_id,
occluded=box.occluded,
z_order=box.z_order,
client_id=start_client_id,
attributes=box.attributes + path.attributes,
)
boxes.append(box)
start_client_id += 1
return self.boxes + boxes, start_client_id
def _to_poly_shapes(self, iter_attr_name, start_client_id):
shapes = []
for path in getattr(self, iter_attr_name):
for shape in path.get_interpolated_shapes():
if not shape.outside:
shape = _LabeledPolyShape(
label=path.label,
points=shape.points,
frame=shape.frame,
group_id=path.group_id,
occluded=shape.occluded,
z_order=shape.z_order,
client_id=start_client_id,
attributes=shape.attributes + path.attributes,
)
shapes.append(shape)
start_client_id += 1
return shapes, start_client_id
def to_polygons(self, start_client_id):
polygons, client_id = self._to_poly_shapes('polygon_paths', start_client_id)
return polygons + self.polygons, client_id
def to_polylines(self, start_client_id):
polylines, client_id = self._to_poly_shapes('polyline_paths', start_client_id)
return polylines + self.polylines, client_id
def to_points(self, start_client_id):
points, client_id = self._to_poly_shapes('points_paths', start_client_id)
return points + self.points, client_id
def to_box_paths(self):
paths = []
for box in self.boxes:
box0 = _InterpolatedBox(box.xtl, box.ytl, box.xbr, box.ybr, box.frame,
box.occluded, box.z_order, False, True)
box1 = copy.copy(box0)
box1.outside = True
box1.frame += 1
path = _BoxPath(
label=box.label,
start_frame=box.frame,
stop_frame=box.frame + 1,
group_id=box.group_id,
boxes=[box0, box1],
attributes=box.attributes,
client_id=box.client_id,
)
paths.append(path)
return self.box_paths + paths
def _to_poly_paths(self, iter_attr_name):
paths = []
for shape in getattr(self, iter_attr_name):
shape0 = _InterpolatedPolyShape(shape.points, shape.frame, shape.occluded, shape.z_order, False, True)
shape1 = copy.copy(shape0)
shape1.outside = True
shape1.frame += 1
path = _PolyPath(
label=shape.label,
start_frame=shape.frame,
stop_frame=shape.frame + 1,
group_id=shape.group_id,
shapes=[shape0, shape1],
client_id=shape.client_id,
attributes=shape.attributes,
)
paths.append(path)
return paths
def to_polygon_paths(self):
return self._to_poly_paths('polygons') + self.polygon_paths
def to_polyline_paths(self):
return self._to_poly_paths('polylines') + self.polyline_paths
def to_points_paths(self):
return self._to_poly_paths('points') + self.points_paths
class _AnnotationForJob(_Annotation):
def __init__(self, db_job):
db_segment = db_job.segment
super().__init__(db_segment.start_frame, db_segment.stop_frame)
# pylint: disable=bad-continuation
self.db_job = db_job
self.logger = slogger.job[db_job.id]
self.db_labels = {db_label.id:db_label
for db_label in db_job.segment.task.label_set.all()}
self.db_attributes = {db_attr.id:db_attr
for db_attr in models.AttributeSpec.objects.filter(
label__task__id=db_job.segment.task.id)}
self.saved_db_ids = {}
self.saved_client_ids = set()
def _collect_saved_ids(self):
self.saved_db_ids = {}
self.saved_client_ids = set()
def append_ids(shape_type, shape_ids):
for db_id, client_id in shape_ids:
self.saved_db_ids[shape_type].append(db_id)
self.saved_client_ids.add(client_id)
for shape_type in ['polygons', 'polylines', 'points', 'boxes', 'paths']:
self.saved_db_ids[shape_type] = []
saved_path_ids = list(self.db_job.objectpath_set.values_list('id', 'client_id'))
append_ids('paths', saved_path_ids)
for shape_type in ['polygons', 'polylines', 'points', 'boxes']:
saved_shapes_ids = list(self._get_shape_class(shape_type).objects.filter(job_id=self.db_job.id).values_list('id', 'client_id'))
append_ids(shape_type, saved_shapes_ids)
def _merge_table_rows(self, rows, keys_for_merge, field_id):
"""dot.notation access to dictionary attributes"""
class dotdict(OrderedDict):
__getattr__ = OrderedDict.get
__setattr__ = OrderedDict.__setitem__
__delattr__ = OrderedDict.__delitem__
__eq__ = lambda self, other: self.id == other.id
__hash__ = lambda self: self.id
# It is necessary to keep a stable order of original rows
# (e.g. for tracked boxes). Otherwise prev_box.frame can be bigger
# than next_box.frame.
merged_rows = OrderedDict()
# Group all rows by field_id. In grouped rows replace fields in
# accordance with keys_for_merge structure.
for row in rows:
row_id = row[field_id]
if not row_id in merged_rows:
merged_rows[row_id] = dotdict(row)
for key in keys_for_merge:
merged_rows[row_id][key] = []
for key in keys_for_merge:
item = dotdict({v.split('__', 1)[-1]:row[v] for v in keys_for_merge[key]})
if item.id:
merged_rows[row_id][key].append(item)
# Remove redundant keys from final objects
redundant_keys = [item for values in keys_for_merge.values() for item in values]
for i in merged_rows:
for j in redundant_keys:
del merged_rows[i][j]
return list(merged_rows.values())
@staticmethod
def _clamp(value, min_value, max_value):
return max(min(value, max_value), min_value)
def _clamp_box(self, xtl, ytl, xbr, ybr, im_size):
xtl = self._clamp(xtl, 0, im_size['width'])
xbr = self._clamp(xbr, 0, im_size['width'])
ytl = self._clamp(ytl, 0, im_size['height'])
ybr = self._clamp(ybr, 0, im_size['height'])
return xtl, ytl, xbr, ybr
def _clamp_poly(self, points, im_size):
verified = []
points = points.split(' ')
for p in points:
p = p.split(',')
verified.append('{},{}'.format(
self._clamp(float(p[0]), 0, im_size['width']),
self._clamp(float(p[1]), 0, im_size['height'])
))
return ' '.join(verified)
def init_from_db(self):
def get_values(shape_type):
if shape_type == 'polygons':
return [
('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledpolygonattributeval__value', 'labeledpolygonattributeval__spec_id',
'labeledpolygonattributeval__id'), {
'attributes': [
'labeledpolygonattributeval__value',
'labeledpolygonattributeval__spec_id',
'labeledpolygonattributeval__id'
]
}, 'labeledpolygonattributeval_set'
]
elif shape_type == 'polylines':
return [
('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledpolylineattributeval__value', 'labeledpolylineattributeval__spec_id',
'labeledpolylineattributeval__id'), {
'attributes': [
'labeledpolylineattributeval__value',
'labeledpolylineattributeval__spec_id',
'labeledpolylineattributeval__id'
]
}, 'labeledpolylineattributeval_set'
]
elif shape_type == 'boxes':
return [
('id', 'frame', 'xtl', 'ytl', 'xbr', 'ybr', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledboxattributeval__value', 'labeledboxattributeval__spec_id',
'labeledboxattributeval__id'), {
'attributes': [
'labeledboxattributeval__value',
'labeledboxattributeval__spec_id',
'labeledboxattributeval__id'
]
}, 'labeledboxattributeval_set'
]
elif shape_type == 'points':
return [
('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledpointsattributeval__value', 'labeledpointsattributeval__spec_id',
'labeledpointsattributeval__id'), {
'attributes': [
'labeledpointsattributeval__value',
'labeledpointsattributeval__spec_id',
'labeledpointsattributeval__id'
]
}, 'labeledpointsattributeval_set'
]
self.reset()
for shape_type in ['boxes', 'points', 'polygons', 'polylines']:
(values, merge_keys, prefetch) = get_values(shape_type)
db_shapes = list(self._get_shape_set(shape_type).prefetch_related(prefetch).
values(*values).order_by('frame'))
db_shapes = self._merge_table_rows(db_shapes, merge_keys, 'id')
for db_shape in db_shapes:
label = _Label(self.db_labels[db_shape.label_id])
if shape_type == 'boxes':
shape = _LabeledBox(label=label,
x0=db_shape.xtl, y0=db_shape.ytl, x1=db_shape.xbr, y1=db_shape.ybr,
frame=db_shape.frame,
group_id=db_shape.group_id,
occluded=db_shape.occluded,
z_order=db_shape.z_order,
client_id=db_shape.client_id,
)
else:
shape = _LabeledPolyShape(
label=label,
points=db_shape.points,
frame=db_shape.frame,
group_id=db_shape.group_id,
occluded=db_shape.occluded,
z_order=db_shape.z_order,
client_id=db_shape.client_id,
)
for db_attr in db_shape.attributes:
if db_attr.id != None:
spec = self.db_attributes[db_attr.spec_id]
attr = _Attribute(spec, db_attr.value)
shape.add_attribute(attr)
getattr(self, shape_type).append(shape)
db_paths = self.db_job.objectpath_set
for shape in ['trackedpoints_set', 'trackedbox_set', 'trackedpolyline_set', 'trackedpolygon_set']:
db_paths.prefetch_related(shape)
for shape_attr in ['trackedpoints_set__trackedpointsattributeval_set', 'trackedbox_set__trackedboxattributeval_set',
'trackedpolygon_set__trackedpolygonattributeval_set', 'trackedpolyline_set__trackedpolylineattributeval_set']:
db_paths.prefetch_related(shape_attr)
db_paths.prefetch_related('objectpathattributeval_set')
db_paths = list (db_paths.values('id', 'frame', 'group_id', 'shapes', 'client_id', 'objectpathattributeval__spec_id',
'objectpathattributeval__id', 'objectpathattributeval__value',
'trackedbox', 'trackedpolygon', 'trackedpolyline', 'trackedpoints',
'trackedbox__id', 'label_id', 'trackedbox__xtl', 'trackedbox__ytl',
'trackedbox__xbr', 'trackedbox__ybr', 'trackedbox__frame', 'trackedbox__occluded',
'trackedbox__z_order','trackedbox__outside', 'trackedbox__trackedboxattributeval__spec_id',
'trackedbox__trackedboxattributeval__value', 'trackedbox__trackedboxattributeval__id',
'trackedpolygon__id' ,'trackedpolygon__points', 'trackedpolygon__frame', 'trackedpolygon__occluded',
'trackedpolygon__z_order', 'trackedpolygon__outside', 'trackedpolygon__trackedpolygonattributeval__spec_id',
'trackedpolygon__trackedpolygonattributeval__value', 'trackedpolygon__trackedpolygonattributeval__id',
'trackedpolyline__id', 'trackedpolyline__points', 'trackedpolyline__frame', 'trackedpolyline__occluded',
'trackedpolyline__z_order', 'trackedpolyline__outside', 'trackedpolyline__trackedpolylineattributeval__spec_id',
'trackedpolyline__trackedpolylineattributeval__value', 'trackedpolyline__trackedpolylineattributeval__id',
'trackedpoints__id', 'trackedpoints__points', 'trackedpoints__frame', 'trackedpoints__occluded',
'trackedpoints__z_order', 'trackedpoints__outside', 'trackedpoints__trackedpointsattributeval__spec_id',
'trackedpoints__trackedpointsattributeval__value', 'trackedpoints__trackedpointsattributeval__id')
.order_by('id', 'trackedbox__frame', 'trackedpolygon__frame', 'trackedpolyline__frame', 'trackedpoints__frame'))
db_box_paths = list(filter(lambda path: path['shapes'] == 'boxes', db_paths ))
db_polygon_paths = list(filter(lambda path: path['shapes'] == 'polygons', db_paths ))
db_polyline_paths = list(filter(lambda path: path['shapes'] == 'polylines', db_paths ))
db_points_paths = list(filter(lambda path: path['shapes'] == 'points', db_paths ))
object_path_attr_merge_key = [
'objectpathattributeval__value',
'objectpathattributeval__spec_id',
'objectpathattributeval__id'
]
db_box_paths = self._merge_table_rows(db_box_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedbox__id', 'trackedbox__xtl', 'trackedbox__ytl',
'trackedbox__xbr', 'trackedbox__ybr', 'trackedbox__frame',
'trackedbox__occluded', 'trackedbox__z_order', 'trackedbox__outside',
'trackedbox__trackedboxattributeval__value',
'trackedbox__trackedboxattributeval__spec_id',
'trackedbox__trackedboxattributeval__id'
],
}, 'id')
db_polygon_paths = self._merge_table_rows(db_polygon_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedpolygon__id', 'trackedpolygon__points', 'trackedpolygon__frame',
'trackedpolygon__occluded', 'trackedpolygon__z_order', 'trackedpolygon__outside',
'trackedpolygon__trackedpolygonattributeval__value',
'trackedpolygon__trackedpolygonattributeval__spec_id',
'trackedpolygon__trackedpolygonattributeval__id'
]
}, 'id')
db_polyline_paths = self._merge_table_rows(db_polyline_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedpolyline__id', 'trackedpolyline__points', 'trackedpolyline__frame',
'trackedpolyline__occluded', 'trackedpolyline__z_order', 'trackedpolyline__outside',
'trackedpolyline__trackedpolylineattributeval__value',
'trackedpolyline__trackedpolylineattributeval__spec_id',
'trackedpolyline__trackedpolylineattributeval__id'
],
}, 'id')
db_points_paths = self._merge_table_rows(db_points_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedpoints__id', 'trackedpoints__points', 'trackedpoints__frame',
'trackedpoints__occluded', 'trackedpoints__z_order', 'trackedpoints__outside',
'trackedpoints__trackedpointsattributeval__value',
'trackedpoints__trackedpointsattributeval__spec_id',
'trackedpoints__trackedpointsattributeval__id'
]
}, 'id')
for db_box_path in db_box_paths:
db_box_path.attributes = list(set(db_box_path.attributes))
db_box_path.shapes = self._merge_table_rows(db_box_path.shapes, {
'attributes': [
'trackedboxattributeval__value',
'trackedboxattributeval__spec_id',
'trackedboxattributeval__id'
]
}, 'id')
for db_polygon_path in db_polygon_paths:
db_polygon_path.attributes = list(set(db_polygon_path.attributes))
db_polygon_path.shapes = self._merge_table_rows(db_polygon_path.shapes, {
'attributes': [
'trackedpolygonattributeval__value',
'trackedpolygonattributeval__spec_id',
'trackedpolygonattributeval__id'
]
}, 'id')
for db_polyline_path in db_polyline_paths:
db_polyline_path.attributes = list(set(db_polyline_path.attributes))
db_polyline_path.shapes = self._merge_table_rows(db_polyline_path.shapes, {
'attributes': [
'trackedpolylineattributeval__value',
'trackedpolylineattributeval__spec_id',
'trackedpolylineattributeval__id'
]
}, 'id')
for db_points_path in db_points_paths:
db_points_path.attributes = list(set(db_points_path.attributes))
db_points_path.shapes = self._merge_table_rows(db_points_path.shapes, {
'attributes': [
'trackedpointsattributeval__value',
'trackedpointsattributeval__spec_id',
'trackedpointsattributeval__id'
]
}, 'id')
for db_path in db_box_paths:
for db_shape in db_path.shapes:
db_shape.attributes = list(set(db_shape.attributes))
label = _Label(self.db_labels[db_path.label_id])
path = _BoxPath(
label=label,
start_frame=db_path.frame,
stop_frame=self.stop_frame,
group_id=db_path.group_id,
client_id=db_path.client_id,
)
for db_attr in db_path.attributes:
spec = self.db_attributes[db_attr.spec_id]
attr = _Attribute(spec, db_attr.value)
path.add_attribute(attr)
frame = -1
for db_shape in db_path.shapes:
box = _TrackedBox(
x0=db_shape.xtl, y0=db_shape.ytl, x1=db_shape.xbr, y1=db_shape.ybr,
frame=db_shape.frame,
occluded=db_shape.occluded,
z_order=db_shape.z_order,
outside=db_shape.outside,
)
assert box.frame > frame
frame = box.frame
for db_attr in db_shape.attributes:
spec = self.db_attributes[db_attr.spec_id]
attr = _Attribute(spec, db_attr.value)
box.add_attribute(attr)
path.add_box(box)
self.box_paths.append(path)
for idx, paths_type in enumerate(['polygon_paths', 'polyline_paths', 'points_paths']):
source = [db_polygon_paths, db_polyline_paths, db_points_paths][idx]
for db_path in source:
for db_shape in db_path.shapes:
db_shape.attributes = list(set(db_shape.attributes))
label = _Label(self.db_labels[db_path.label_id])
path = _PolyPath(
label=label,
start_frame=db_path.frame,
stop_frame= self.stop_frame,
group_id=db_path.group_id,
client_id=db_path.client_id,
)
for db_attr in db_path.attributes:
spec = self.db_attributes[db_attr.spec_id]
attr = _Attribute(spec, db_attr.value)
path.add_attribute(attr)
frame = -1
for db_shape in db_path.shapes:
shape = _TrackedPolyShape(
points=db_shape.points,
frame=db_shape.frame,
occluded=db_shape.occluded,
z_order=db_shape.z_order,
outside=db_shape.outside,
)
assert shape.frame > frame
frame = shape.frame
for db_attr in db_shape.attributes:
spec = self.db_attributes[db_attr.spec_id]
attr = _Attribute(spec, db_attr.value)
shape.add_attribute(attr)
path.add_shape(shape)
getattr(self, paths_type).append(path)
def init_from_client(self, data):
# All fields inside data should be converted to correct type explicitly.
# We cannot trust that client will send 23 as integer. Here we also
# accept "23".
db_task = self.db_job.segment.task
image_meta = get_image_meta_cache(db_task)
self.reset()
for box in data['boxes']:
label = _Label(self.db_labels[int(box['label_id'])])
frame_idx = int(box['frame']) if db_task.mode == 'annotation' else 0
xtl, ytl, xbr, ybr = self._clamp_box(float(box['xtl']), float(box['ytl']),
float(box['xbr']), float(box['ybr']),
image_meta['original_size'][frame_idx])
labeled_box = _LabeledBox(
label=label,
x0=xtl, y0=ytl, x1=xbr, y1=ybr,
frame=int(box['frame']),
group_id=int(box['group_id']),
occluded=strtobool(str(box['occluded'])),
z_order=int(box['z_order']),
client_id=int(box['id']),
)
for attr in box['attributes']:
spec = self.db_attributes[int(attr['id'])]
attr = _Attribute(spec, str(attr['value']))
labeled_box.add_attribute(attr)
self.boxes.append(labeled_box)
for poly_shape_type in ['points', 'polygons', 'polylines']:
for poly_shape in data[poly_shape_type]:
label = _Label(self.db_labels[int(poly_shape['label_id'])])
frame_idx = int(poly_shape['frame']) if db_task.mode == 'annotation' else 0
points = self._clamp_poly(poly_shape['points'], image_meta['original_size'][frame_idx])
labeled_poly_shape = _LabeledPolyShape(
label=label,
points=points,
frame=int(poly_shape['frame']),
group_id=int(poly_shape['group_id']),
occluded=poly_shape['occluded'],
z_order=int(poly_shape['z_order']),
client_id=int(poly_shape['id']),
)
for attr in poly_shape['attributes']:
spec = self.db_attributes[int(attr['id'])]
attr = _Attribute(spec, str(attr['value']))
labeled_poly_shape.add_attribute(attr)
getattr(self, poly_shape_type).append(labeled_poly_shape)
for path in data['box_paths']:
label = _Label(self.db_labels[int(path['label_id'])])
boxes = []
frame = -1
has_boxes_on_prev_segm = False
last_box_on_prev_segm = None
has_box_on_start_frame = False
for box in path['shapes']:
if int(box['frame']) < self.start_frame:
has_boxes_on_prev_segm = True
if last_box_on_prev_segm is None or int(last_box_on_prev_segm["frame"]) < int(box["frame"]):
last_box_on_prev_segm = box
elif int(box['frame']) == self.start_frame:
has_box_on_start_frame = True
break
if has_boxes_on_prev_segm and not has_box_on_start_frame:
last_box_on_prev_segm["frame"] = self.start_frame
for box in path['shapes']:
if int(box['frame']) <= self.stop_frame and int(box['frame']) >= self.start_frame:
frame_idx = int(box['frame']) if db_task.mode == 'annotation' else 0
xtl, ytl, xbr, ybr = self._clamp_box(float(box['xtl']), float(box['ytl']),
float(box['xbr']), float(box['ybr']), image_meta['original_size'][frame_idx])
tracked_box = _TrackedBox(
x0=xtl, y0=ytl, x1=xbr, y1=ybr,
frame=int(box['frame']),
occluded=strtobool(str(box['occluded'])),
z_order=int(box['z_order']),
outside=strtobool(str(box['outside'])),
)
assert tracked_box.frame > frame
frame = tracked_box.frame
for attr in box['attributes']:
spec = self.db_attributes[int(attr['id'])]
assert spec.is_mutable()
attr = _Attribute(spec, str(attr['value']))
tracked_box.add_attribute(attr)
boxes.append(tracked_box)
else:
self.logger.error("init_from_client: ignore frame #%d " +
"because it out of segment range [%d-%d]", int(box['frame']), self.start_frame, self.stop_frame)
attributes = []
for attr in path['attributes']:
spec = self.db_attributes[int(attr['id'])]
assert not spec.is_mutable()
attr = _Attribute(spec, str(attr['value']))
attributes.append(attr)
assert frame <= self.stop_frame
box_path = _BoxPath(label=label,
start_frame=min(list(map(lambda box: box.frame, boxes))),
stop_frame=self.stop_frame,
group_id=int(path['group_id']),
boxes=boxes,
client_id=int(path['id']),
attributes=attributes,
)
self.box_paths.append(box_path)
for poly_path_type in ['points_paths', 'polygon_paths', 'polyline_paths']:
for path in data[poly_path_type]:
label = _Label(self.db_labels[int(path['label_id'])])
poly_shapes = []
frame = -1
has_shapes_on_prev_segm = False
last_shape_on_prev_segm = None
has_shape_on_start_frame = False
for poly_shape in path['shapes']:
if int(poly_shape['frame']) < self.start_frame:
has_shapes_on_prev_segm = True
if last_shape_on_prev_segm is None or int(last_shape_on_prev_segm["frame"]) < (poly_shape["frame"]):
last_shape_on_prev_segm = box
elif int(poly_shape['frame']) == self.start_frame:
has_shape_on_start_frame = True
break
if has_shapes_on_prev_segm and not has_shape_on_start_frame:
last_shape_on_prev_segm["frame"] = self.start_frame
for poly_shape in path['shapes']:
if int(poly_shape['frame']) <= self.stop_frame and int(poly_shape['frame']) >= self.start_frame:
frame_idx = int(poly_shape['frame']) if db_task.mode == 'annotation' else 0
points = self._clamp_poly(poly_shape['points'], image_meta['original_size'][frame_idx])
tracked_poly_shape = _TrackedPolyShape(
points=points,
frame=int(poly_shape['frame']),
occluded=strtobool(str(poly_shape['occluded'])),
z_order=int(poly_shape['z_order']),
outside=strtobool(str(poly_shape['outside'])),
)
assert tracked_poly_shape.frame > frame
frame = tracked_poly_shape.frame
for attr in poly_shape['attributes']:
spec = self.db_attributes[int(attr['id'])]
assert spec.is_mutable()
attr = _Attribute(spec, str(attr['value']))
tracked_poly_shape.add_attribute(attr)
poly_shapes.append(tracked_poly_shape)
else:
self.logger.error("init_from_client: ignore frame #%d " +
"because it out of segment range [%d-%d]", int(poly_shape['frame']), self.start_frame, self.stop_frame)
attributes = []
for attr in path['attributes']:
spec = self.db_attributes[int(attr['id'])]
assert not spec.is_mutable()
attr = _Attribute(spec, str(attr['value']))
attributes.append(attr)
poly_path = _PolyPath(
label=label,
start_frame=min(list(map(lambda shape: shape.frame, poly_shapes))),
stop_frame=self.stop_frame + 1,
group_id=int(path['group_id']),
shapes=poly_shapes,
client_id=int(path['id']),
attributes=attributes,
)
getattr(self, poly_path_type).append(poly_path)
def _get_shape_class(self, shape_type):
if shape_type == 'polygons':
return models.LabeledPolygon
elif shape_type == 'polylines':
return models.LabeledPolyline
elif shape_type == 'boxes':
return models.LabeledBox
elif shape_type == 'points':
return models.LabeledPoints
elif shape_type == 'polygon_paths':
return models.TrackedPolygon
elif shape_type == 'polyline_paths':
return models.TrackedPolyline
elif shape_type == 'box_paths':
return models.TrackedBox
elif shape_type == 'points_paths':
return models.TrackedPoints
def _get_shape_attr_class(self, shape_type):
if shape_type == 'polygons':
return models.LabeledPolygonAttributeVal
elif shape_type == 'polylines':
return models.LabeledPolylineAttributeVal
elif shape_type == 'boxes':
return models.LabeledBoxAttributeVal
elif shape_type == 'points':
return models.LabeledPointsAttributeVal
elif shape_type == 'polygon_paths':
return models.TrackedPolygonAttributeVal
elif shape_type == 'polyline_paths':
return models.TrackedPolylineAttributeVal
elif shape_type == 'box_paths':
return models.TrackedBoxAttributeVal
elif shape_type == 'points_paths':
return models.TrackedPointsAttributeVal
def _save_paths_to_db(self):
for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']:
db_paths = []
db_path_attrvals = []
db_shapes = []
db_shape_attrvals = []
# Need to be sure saved_db_ids is actual.
self._collect_saved_ids()
shapes = getattr(self, shape_type)
for path in shapes:
db_path = models.ObjectPath()
db_path.job = self.db_job
db_path.label = self.db_labels[path.label.id]
db_path.frame = path.frame
db_path.group_id = path.group_id
db_path.client_id = path.client_id
if shape_type == 'polygon_paths':
db_path.shapes = 'polygons'
elif shape_type == 'polyline_paths':
db_path.shapes = 'polylines'
elif shape_type == 'box_paths':
db_path.shapes = 'boxes'
elif shape_type == 'points_paths':
db_path.shapes = 'points'
for attr in path.attributes:
db_attrspec = self.db_attributes[attr.id]
db_attrval = models.ObjectPathAttributeVal()
db_attrval.track_id = len(db_paths)
db_attrval.spec = db_attrspec
db_attrval.value = attr.value
db_path_attrvals.append(db_attrval)
path_shapes = path.boxes if hasattr(path, 'boxes') else path.shapes
for shape in path_shapes:
db_shape = self._get_shape_class(shape_type)()
db_shape.track_id = len(db_paths)
if shape_type == 'box_paths':
db_shape.xtl = shape.xtl
db_shape.ytl = shape.ytl
db_shape.xbr = shape.xbr
db_shape.ybr = shape.ybr
else:
db_shape.points = shape.points
db_shape.frame = shape.frame
db_shape.occluded = shape.occluded
db_shape.z_order = shape.z_order
db_shape.outside = shape.outside
for attr in shape.attributes:
db_attrspec = self.db_attributes[attr.id]
db_attrval = self._get_shape_attr_class(shape_type)()
if shape_type == 'polygon_paths':
db_attrval.polygon_id = len(db_shapes)
elif shape_type == 'polyline_paths':
db_attrval.polyline_id = len(db_shapes)
elif shape_type == 'box_paths':
db_attrval.box_id = len(db_shapes)
elif shape_type == 'points_paths':
db_attrval.points_id = len(db_shapes)
db_attrval.spec = db_attrspec
db_attrval.value = attr.value
db_shape_attrvals.append(db_attrval)
db_shapes.append(db_shape)
db_paths.append(db_path)
db_paths = models.ObjectPath.objects.bulk_create(db_paths)
if db_paths and db_paths[0].id == None:
# Try to get primary keys. Probably the code will work for sqlite
# but it definetely doesn't work for Postgres. Need to say that
# for Postgres bulk_create will return objects with ids even ids
# are auto incremented. Thus we will not be inside the 'if'.
if shape_type == 'polygon_paths':
db_paths = list(self.db_job.objectpath_set.exclude(id__in=self.saved_db_ids['paths']))
elif shape_type == 'polyline_paths':
db_paths = list(self.db_job.objectpath_set.exclude(id__in=self.saved_db_ids['paths']))
elif shape_type == 'box_paths':
db_paths = list(self.db_job.objectpath_set.exclude(id__in=self.saved_db_ids['paths']))
elif shape_type == 'points_paths':
db_paths = list(self.db_job.objectpath_set.exclude(id__in=self.saved_db_ids['paths']))
for db_attrval in db_path_attrvals:
db_attrval.track_id = db_paths[db_attrval.track_id].id
models.ObjectPathAttributeVal.objects.bulk_create(db_path_attrvals)
for db_shape in db_shapes:
db_shape.track_id = db_paths[db_shape.track_id].id
db_shapes_ids = list(self._get_shape_class(shape_type).objects.filter(track__job_id=self.db_job.id).values_list('id', flat=True))
db_shapes = self._get_shape_class(shape_type).objects.bulk_create(db_shapes)
if db_shapes and db_shapes[0].id == None:
# Try to get primary keys. Probably the code will work for sqlite
# but it definetely doesn't work for Postgres. Need to say that
# for Postgres bulk_create will return objects with ids even ids
# are auto incremented. Thus we will not be inside the 'if'.
db_shapes = list(self._get_shape_class(shape_type).objects.exclude(id__in=db_shapes_ids).filter(track__job_id=self.db_job.id))
for db_attrval in db_shape_attrvals:
if shape_type == 'polygon_paths':
db_attrval.polygon_id = db_shapes[db_attrval.polygon_id].id
elif shape_type == 'polyline_paths':
db_attrval.polyline_id = db_shapes[db_attrval.polyline_id].id
elif shape_type == 'box_paths':
db_attrval.box_id = db_shapes[db_attrval.box_id].id
elif shape_type == 'points_paths':
db_attrval.points_id = db_shapes[db_attrval.points_id].id
self._get_shape_attr_class(shape_type).objects.bulk_create(db_shape_attrvals)
def _get_shape_set(self, shape_type):
if shape_type == 'polygons':
return self.db_job.labeledpolygon_set
elif shape_type == 'polylines':
return self.db_job.labeledpolyline_set
elif shape_type == 'boxes':
return self.db_job.labeledbox_set
elif shape_type == 'points':
return self.db_job.labeledpoints_set
def _save_shapes_to_db(self):
# Need to be sure saved_db_ids is actual.
self._collect_saved_ids()
for shape_type in ['polygons', 'polylines', 'points', 'boxes']:
db_shapes = []
db_attrvals = []
shapes = getattr(self, shape_type)
for shape in shapes:
db_shape = self._get_shape_class(shape_type)()
db_shape.job = self.db_job
db_shape.label = self.db_labels[shape.label.id]
db_shape.group_id = shape.group_id
db_shape.client_id = shape.client_id
if shape_type == 'boxes':
db_shape.xtl = shape.xtl
db_shape.ytl = shape.ytl
db_shape.xbr = shape.xbr
db_shape.ybr = shape.ybr
else:
db_shape.points = shape.points
db_shape.frame = shape.frame
db_shape.occluded = shape.occluded
db_shape.z_order = shape.z_order
for attr in shape.attributes:
db_attrval = self._get_shape_attr_class(shape_type)()
if shape_type == 'polygons':
db_attrval.polygon_id = len(db_shapes)
elif shape_type == 'polylines':
db_attrval.polyline_id = len(db_shapes)
elif shape_type == 'boxes':
db_attrval.box_id = len(db_shapes)
else:
db_attrval.points_id = len(db_shapes)
db_attrval.spec = self.db_attributes[attr.id]
db_attrval.value = attr.value
db_attrvals.append(db_attrval)
db_shapes.append(db_shape)
db_shapes = self._get_shape_class(shape_type).objects.bulk_create(db_shapes)
if db_shapes and db_shapes[0].id == None:
# Try to get primary keys. Probably the code will work for sqlite
# but it definetely doesn't work for Postgres. Need to say that
# for Postgres bulk_create will return objects with ids even ids
# are auto incremented. Thus we will not be inside the 'if'.
db_shapes = list(self._get_shape_set(shape_type).exclude(id__in=self.saved_db_ids[shape_type]))
for db_attrval in db_attrvals:
if shape_type == 'polygons':
db_attrval.polygon_id = db_shapes[db_attrval.polygon_id].id
elif shape_type == 'polylines':
db_attrval.polyline_id = db_shapes[db_attrval.polyline_id].id
elif shape_type == 'boxes':
db_attrval.box_id = db_shapes[db_attrval.box_id].id
else:
db_attrval.points_id = db_shapes[db_attrval.points_id].id
self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals)
def _update_shapes_in_db(self):
client_ids_to_delete = {}
for shape_type in ['polygons', 'polylines', 'points', 'boxes']:
client_ids_to_delete[shape_type] = list(shape.client_id for shape in getattr(self, shape_type))
self._delete_shapes_from_db(client_ids_to_delete)
self._save_shapes_to_db()
def _update_paths_in_db(self):
client_ids_to_delete = {}
for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']:
client_ids_to_delete[shape_type] = list(shape.client_id for shape in getattr(self, shape_type))
self._delete_paths_from_db(client_ids_to_delete)
self._save_paths_to_db()
def _delete_shapes_from_db(self, data):
for shape_type in ['polygons', 'polylines', 'points', 'boxes']:
client_ids_to_delete = data[shape_type]
deleted = self._get_shape_set(shape_type).filter(client_id__in=client_ids_to_delete).delete()
class_name = 'engine.{}'.format(self._get_shape_class(shape_type).__name__)
if not (deleted[0] == 0 and len(client_ids_to_delete) == 0) and (class_name in deleted[1] and deleted[1][class_name] != len(client_ids_to_delete)):
raise Exception('Number of deleted object doesn\'t match with requested number')
def _delete_paths_from_db(self, data):
for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']:
client_ids_to_delete = data[shape_type]
deleted = self.db_job.objectpath_set.filter(client_id__in=client_ids_to_delete).delete()
class_name = 'engine.ObjectPath'
if not (deleted[0] == 0 and len(client_ids_to_delete) == 0) and \
(class_name in deleted[1] and deleted[1][class_name] != len(client_ids_to_delete)):
raise Exception('Number of deleted object doesn\'t match with requested number')
def _delete_all_shapes_from_db(self):
for shape_type in ['polygons', 'polylines', 'points', 'boxes']:
self._get_shape_set(shape_type).all().delete()
def _delete_all_paths_from_db(self):
self.db_job.objectpath_set.all().delete()
def delete_all_shapes_from_db(self):
self._delete_all_shapes_from_db()
self._delete_all_paths_from_db()
def delete_from_db(self, data):
self._delete_shapes_from_db(data)
self._delete_paths_from_db(data)
def update_in_db(self, data):
self.init_from_client(data)
self._update_shapes_in_db()
self._update_paths_in_db()
def save_to_db(self, data):
self.init_from_client(data)
self._save_shapes_to_db()
self._save_paths_to_db()
def to_client(self):
data = {
"boxes": [],
"box_paths": [],
"polygons": [],
"polygon_paths": [],
"polylines": [],
"polyline_paths": [],
"points": [],
"points_paths": []
}
for box in self.boxes:
data["boxes"].append({
"id": box.client_id,
"label_id": box.label.id,
"group_id": box.group_id,
"xtl": box.xtl,
"ytl": box.ytl,
"xbr": box.xbr,
"ybr": box.ybr,
"occluded": box.occluded,
"z_order": box.z_order,
"frame": box.frame,
"attributes": [{'id': attr.id, 'value':attr.value} for attr in box.attributes],
})
for poly_type in ['polygons', 'polylines', 'points']:
for poly in getattr(self, poly_type):
data[poly_type].append({
"id": poly.client_id,
"label_id": poly.label.id,
"group_id": poly.group_id,
"points": poly.points,
"occluded": poly.occluded,
"z_order": poly.z_order,
"frame": poly.frame,
"attributes": [{'id': attr.id, 'value':attr.value} for attr in poly.attributes],
})
for box_path in self.box_paths:
data["box_paths"].append({
"id": box_path.client_id,
"label_id": box_path.label.id,
"group_id": box_path.group_id,
"frame": box_path.frame,
"attributes": [{'id': attr.id, 'value':attr.value} for attr in box_path.attributes],
"shapes": [box for box in map(lambda box:
({
"frame": box.frame,
"xtl": box.xtl,
"ytl": box.ytl,
"xbr": box.xbr,
"ybr": box.ybr,
"occluded": box.occluded,
"z_order": box.z_order,
"outside": box.outside,
"attributes": [{'id': attr.id, 'value':attr.value} for attr in box.attributes],
}), box_path.boxes)
],
})
for poly_path_type in ['polygon_paths', 'polyline_paths', 'points_paths']:
for poly_path in getattr(self, poly_path_type):
data[poly_path_type].append({
"id": poly_path.client_id,
"label_id": poly_path.label.id,
"group_id": poly_path.group_id,
"frame": poly_path.frame,
"attributes": [{'id': attr.id, 'value':attr.value} for attr in poly_path.attributes],
"shapes": [shape for shape in map(lambda shape:
({
"frame": shape.frame,
"points": shape.points,
"occluded": shape.occluded,
"z_order": shape.z_order,
"outside": shape.outside,
"attributes": [{'id': attr.id, 'value':attr.value} for attr in shape.attributes],
}), poly_path.shapes)
],
})
return data
def validate_data_from_client(self, data):
self._collect_saved_ids()
client_ids = {
'create': set(),
'update': set(),
'delete': set(),
}
def extract_clinet_id(shape, action):
if action != 'delete':
if 'id' not in shape:
raise Exception('No id field in received data')
client_id = shape['id']
else:
# client send only shape.id, not shape object
client_id = shape
client_ids[action].add(client_id)
shape_types = ['boxes', 'points', 'polygons', 'polylines', 'box_paths',
'points_paths', 'polygon_paths', 'polyline_paths']
for action in ['create', 'update', 'delete']:
for shape_type in shape_types:
for shape in data[action][shape_type]:
extract_clinet_id(shape, action)
# In case of delete action potentially it is possible to intersect set of IDs
# that should delete and set of IDs that should create(i.e. save uploaded anno).
# There is no need to check that
tmp_res = (client_ids['create'] & client_ids['update']) | (client_ids['update'] & client_ids['delete'])
if tmp_res:
raise Exception('More than one action for shape(s) with id={}'.format(tmp_res))
tmp_res = (self.saved_client_ids - client_ids['delete']) & client_ids['create']
if tmp_res:
raise Exception('Trying to create new shape(s) with existing client id {}'.format(tmp_res))
tmp_res = client_ids['delete'] - self.saved_client_ids
if tmp_res:
raise Exception('Trying to delete shape(s) with nonexistent client id {}'.format(tmp_res))
tmp_res = client_ids['update'] - (self.saved_client_ids - client_ids['delete'])
if tmp_res:
raise Exception('Trying to update shape(s) with nonexistent client id {}'.format(tmp_res))
class _AnnotationForSegment(_Annotation):
def __init__(self, db_segment):
super().__init__(db_segment.start_frame, db_segment.stop_frame)
self.db_segment = db_segment
def init_from_db(self):
# FIXME: at the moment a segment has only one job always. Thus
# the implementation makes sense. Need to implement a good one
# in the future.
self.reset()
db_job0 = list(self.db_segment.job_set.all())[0]
annotation = _AnnotationForJob(db_job0)
annotation.init_from_db()
self.boxes = annotation.boxes
self.box_paths = annotation.box_paths
self.polygons = annotation.polygons
self.polygon_paths = annotation.polygon_paths
self.polylines = annotation.polylines
self.polyline_paths = annotation.polyline_paths
self.points = annotation.points
self.points_paths = annotation.points_paths
@transaction.atomic
def _dump(tid, data_format, scheme, host):
db_task = models.Task.objects.select_for_update().get(id=tid)
annotation = _AnnotationForTask(db_task)
annotation.init_from_db()
annotation.dump(data_format, scheme, host)
def _calc_box_area(box):
return (box.xbr - box.xtl) * (box.ybr - box.ytl)
def _calc_overlap_box_area(box0, box1):
dx = min(box0.xbr, box1.xbr) - max(box0.xtl, box1.xtl)
dy = min(box0.ybr, box1.ybr) - max(box0.ytl, box1.ytl)
if dx > 0 and dy > 0:
return dx * dy
else:
return 0
def _calc_box_IoU(box0, box1):
overlap_area = _calc_overlap_box_area(box0, box1)
return overlap_area / (_calc_box_area(box0) + _calc_box_area(box1) - overlap_area)
class _AnnotationWriter:
__metaclass__ = ABCMeta
def __init__(self, file, version):
self.version = version
self.file = file
@abstractmethod
def open_root(self):
raise NotImplementedError
@abstractmethod
def add_meta(self, meta):
raise NotImplementedError
@abstractmethod
def open_track(self, track):
raise NotImplementedError
@abstractmethod
def open_image(self, image):
raise NotImplementedError
@abstractmethod
def open_box(self, box):
raise NotImplementedError
@abstractmethod
def open_polygon(self, polygon):
raise NotImplementedError
@abstractmethod
def open_polyline(self, polyline):
raise NotImplementedError
@abstractmethod
def open_points(self, points):
raise NotImplementedError
@abstractmethod
def add_attribute(self, attribute):
raise NotImplementedError
@abstractmethod
def close_box(self):
raise NotImplementedError
@abstractmethod
def close_polygon(self):
raise NotImplementedError
@abstractmethod
def close_polyline(self):
raise NotImplementedError
@abstractmethod
def close_points(self):
raise NotImplementedError
@abstractmethod
def close_image(self):
raise NotImplementedError
@abstractmethod
def close_track(self):
raise NotImplementedError
@abstractmethod
def close_root(self):
raise NotImplementedError
class _XmlAnnotationWriter(_AnnotationWriter):
def __init__(self, file):
super().__init__(file, "1.1")
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 type(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()
class _AnnotationForTask(_Annotation):
def __init__(self, db_task):
super().__init__(0, db_task.size)
self.db_task = db_task
def init_from_db(self):
self.reset()
for db_segment in self.db_task.segment_set.all():
annotation = _AnnotationForSegment(db_segment)
annotation.init_from_db()
self._merge_boxes(annotation.boxes, db_segment.start_frame,
self.db_task.overlap)
self._merge_paths(annotation.box_paths, db_segment.start_frame,
self.db_task.overlap)
self.polygons.extend(annotation.polygons)
self.polylines.extend(annotation.polylines)
self.points.extend(annotation.points)
self.polygon_paths.extend(annotation.polygon_paths)
self.polyline_paths.extend(annotation.polyline_paths)
self.points_paths.extend(annotation.points_paths)
# FIXME PolyShapes merge???
def _merge_paths(self, paths, start_frame, overlap):
# 1. Split paths on two parts: new and which can be intersected
# with existing paths.
new_paths = [path for path in paths
if path.frame >= start_frame + overlap]
int_paths = [path for path in paths
if path.frame < start_frame + overlap]
assert len(new_paths) + len(int_paths) == len(paths)
# 4. Find old paths which are intersected with int_paths
old_paths = []
for path in self.box_paths:
box = path.get_interpolated_boxes()[-1]
if box.frame >= start_frame:
old_paths.append(path)
# 3. Add new paths as is. It should be done only after old_paths
# variable is initialized.
self.box_paths.extend(new_paths)
# Nothing to merge. Just add all int_paths if any.
if not old_paths or not int_paths:
self.box_paths.extend(int_paths)
return
# 4. Build cost matrix for each path and find correspondence using
# Hungarian algorithm.
min_cost_thresh = 0.5
cost_matrix = np.empty(shape=(len(int_paths), len(old_paths)),
dtype=float)
for i, int_path in enumerate(int_paths):
for j, old_path in enumerate(old_paths):
cost_matrix[i][j] = 1
if int_path.label.id == old_path.label.id:
# Here start_frame is the start frame of next segment
# and stop_frame is the stop frame of current segment
stop_frame = start_frame + overlap - 1
int_boxes = int_path.get_interpolated_boxes()
old_boxes = old_path.get_interpolated_boxes()
int_boxes = {box.frame:box for box in int_boxes if box.frame <= stop_frame}
old_boxes = {box.frame:box for box in old_boxes if box.frame >= start_frame}
assert int_boxes and old_boxes
count, error = 0, 0
for frame in range(start_frame, stop_frame + 1):
box0, box1 = int_boxes.get(frame), old_boxes.get(frame)
if box0 and box1:
if box0.outside != box1.outside:
error += 1
else:
error += 1 - _calc_box_IoU(box0, box1)
count += 1
elif box0 or box1:
error += 1
count += 1
cost_matrix[i][j] = error / count
# 6. Find optimal solution using Hungarian algorithm.
row_ind, col_ind = linear_sum_assignment(cost_matrix)
int_paths_indexes = list(range(0, len(int_paths)))
for i, j in zip(row_ind, col_ind):
# Reject the solution if the cost is too high. Remember
# inside int_boxes_indexes boxes which were handled.
if cost_matrix[i][j] <= min_cost_thresh:
old_paths[j].merge(int_paths[i])
int_paths_indexes[i] = -1
# 7. Add all paths which were not processed.
for i in int_paths_indexes:
if i != -1:
self.box_paths.append(int_paths[i])
def _merge_boxes(self, boxes, start_frame, overlap):
# 1. Split boxes on two parts: new and which can be intersected
# with existing boxes.
new_boxes = [box for box in boxes
if box.frame >= start_frame + overlap]
int_boxes = [box for box in boxes
if box.frame < start_frame + overlap]
assert len(new_boxes) + len(int_boxes) == len(boxes)
# 2. Convert to more convenient data structure (boxes by frame)
int_boxes_by_frame = {}
for box in int_boxes:
if box.frame in int_boxes_by_frame:
int_boxes_by_frame[box.frame].append(box)
else:
int_boxes_by_frame[box.frame] = [box]
old_boxes_by_frame = {}
for box in self.boxes:
if box.frame >= start_frame:
if box.frame in old_boxes_by_frame:
old_boxes_by_frame[box.frame].append(box)
else:
old_boxes_by_frame[box.frame] = [box]
# 3. Add new boxes as is. It should be done only after old_boxes_by_frame
# variable is initialized.
self.boxes.extend(new_boxes)
# Nothing to merge here. Just add all int_boxes if any.
if not old_boxes_by_frame or not int_boxes_by_frame:
self.boxes.extend(int_boxes)
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 = 0.25
for frame in int_boxes_by_frame:
if frame in old_boxes_by_frame:
int_boxes = int_boxes_by_frame[frame]
old_boxes = old_boxes_by_frame[frame]
cost_matrix = np.empty(shape=(len(int_boxes), len(old_boxes)),
dtype=float)
# 5.1 Construct cost matrix for the frame.
for i, box0 in enumerate(int_boxes):
for j, box1 in enumerate(old_boxes):
if box0.label.id == box1.label.id:
cost_matrix[i][j] = 1 - _calc_box_IoU(box0, box1)
else:
cost_matrix[i][j] = 1
# 6. Find optimal solution using Hungarian algorithm.
row_ind, col_ind = linear_sum_assignment(cost_matrix)
int_boxes_indexes = list(range(0, len(int_boxes)))
for i, j in zip(row_ind, col_ind):
# Reject the solution if the cost is too high. Remember
# inside int_boxes_indexes boxes which were handled.
if cost_matrix[i][j] <= min_cost_thresh:
old_boxes[j].merge(int_boxes[i])
int_boxes_indexes[i] = -1
# 7. Add all boxes which were not processed.
for i in int_boxes_indexes:
if i != -1:
self.boxes.append(int_boxes[i])
else:
# We don't have old boxes on the frame. Let's add all new ones.
self.boxes.extend(int_boxes_by_frame[frame])
def dump(self, data_format, scheme, host):
def _flip_box(box, im_w, im_h):
box.xbr, box.xtl = im_w - box.xtl, im_w - box.xbr
box.ybr, box.ytl = im_h - box.ytl, im_h - box.ybr
def _flip_shape(shape, im_w, im_h):
points = []
for p in shape.points.split(' '):
p = p.split(',')
points.append({
'x': p[0],
'y': p[1]
})
for p in points:
p['x'] = im_w - (float(p['x']) + 1)
p['y'] = im_h - (float(p['y']) + 1)
shape.points = ' '.join(['{},{}'.format(point['x'], point['y']) for point in points])
db_task = self.db_task
db_segments = db_task.segment_set.all().prefetch_related('job_set')
db_labels = db_task.label_set.all().prefetch_related('attributespec_set')
im_meta_data = get_image_meta_cache(db_task)
meta = OrderedDict([
("task", OrderedDict([
("id", str(db_task.id)),
("name", db_task.name),
("size", str(db_task.size)),
("mode", db_task.mode),
("overlap", str(db_task.overlap)),
("bugtracker", db_task.bug_tracker),
("flipped", str(db_task.flipped)),
("created", str(timezone.localtime(db_task.created_date))),
("updated", str(timezone.localtime(db_task.updated_date))),
("source", db_task.source),
("labels", [
("label", OrderedDict([
("name", db_label.name),
("attributes", [("attribute", db_attr.text)
for db_attr in db_label.attributespec_set.all()])
])) for db_label in db_labels
]),
("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(
scheme, host, db_segment.job_set.all()[0].id))
])) for db_segment in db_segments
]),
("owner", OrderedDict([
("username", db_task.owner.username),
("email", db_task.owner.email)
]) if db_task.owner else ""),
])),
("dumped", str(timezone.localtime(timezone.now())))
])
if db_task.mode == "interpolation":
meta["task"]["original_size"] = OrderedDict([
("width", str(im_meta_data["original_size"][0]["width"])),
("height", str(im_meta_data["original_size"][0]["height"]))
])
dump_path = db_task.get_dump_path()
with open(dump_path, "w") as dump_file:
dumper = _XmlAnnotationWriter(dump_file)
dumper.open_root()
dumper.add_meta(meta)
if db_task.mode == "annotation":
shapes = {}
shapes["boxes"] = {}
shapes["polygons"] = {}
shapes["polylines"] = {}
shapes["points"] = {}
boxes, max_client_id = self.to_boxes(self.get_max_client_id() + 1)
for box in boxes:
if box.frame not in shapes["boxes"]:
shapes["boxes"][box.frame] = []
shapes["boxes"][box.frame].append(box)
polygons, max_client_id = self.to_polygons(max_client_id)
for polygon in polygons:
if polygon.frame not in shapes["polygons"]:
shapes["polygons"][polygon.frame] = []
shapes["polygons"][polygon.frame].append(polygon)
polylines, max_client_id = self.to_polylines(max_client_id)
for polyline in polylines:
if polyline.frame not in shapes["polylines"]:
shapes["polylines"][polyline.frame] = []
shapes["polylines"][polyline.frame].append(polyline)
points, max_client_id = self.to_points(max_client_id)
for points in points:
if points.frame not in shapes["points"]:
shapes["points"][points.frame] = []
shapes["points"][points.frame].append(points)
for frame in sorted(set(list(shapes["boxes"].keys()) +
list(shapes["polygons"].keys()) +
list(shapes["polylines"].keys()) +
list(shapes["points"].keys()))):
link = get_frame_path(db_task.id, frame)
path = os.readlink(link)
rpath = path.split(os.path.sep)
rpath = os.path.sep.join(rpath[rpath.index(".upload")+1:])
im_w = im_meta_data['original_size'][frame]['width']
im_h = im_meta_data['original_size'][frame]['height']
dumper.open_image(OrderedDict([
("id", str(frame)),
("name", rpath),
("width", str(im_meta_data['original_size'][frame]["width"])),
("height", str(im_meta_data['original_size'][frame]["height"]))
]))
for shape_type in ["boxes", "polygons", "polylines", "points"]:
shape_dict = shapes[shape_type]
if frame in shape_dict:
for shape in shape_dict[frame]:
if shape_type == "boxes":
if db_task.flipped:
_flip_box(shape, im_w, im_h)
dump_dict = OrderedDict([
("label", shape.label.name),
("xtl", "{:.2f}".format(shape.xtl)),
("ytl", "{:.2f}".format(shape.ytl)),
("xbr", "{:.2f}".format(shape.xbr)),
("ybr", "{:.2f}".format(shape.ybr)),
("occluded", str(int(shape.occluded))),
("id", str(shape.client_id)),
])
if db_task.z_order:
dump_dict['z_order'] = str(shape.z_order)
if shape.group_id:
dump_dict['group_id'] = str(shape.group_id)
dumper.open_box(dump_dict)
else:
if db_task.flipped:
_flip_shape(shape, im_w, im_h)
dump_dict = OrderedDict([
("label", shape.label.name),
("points", ';'.join((
','.join((
"{:.2f}".format(float(p.split(',')[0])),
"{:.2f}".format(float(p.split(',')[1]))
)) for p in shape.points.split(' '))
)),
("occluded", str(int(shape.occluded))),
("id", str(shape.client_id)),
])
if db_task.z_order:
dump_dict['z_order'] = str(shape.z_order)
if shape.group_id:
dump_dict['group_id'] = str(shape.group_id)
if shape_type == "polygons":
dumper.open_polygon(dump_dict)
elif shape_type == "polylines":
dumper.open_polyline(dump_dict)
else:
dumper.open_points(dump_dict)
for attr in shape.attributes:
dumper.add_attribute(OrderedDict([
("name", attr.name),
("value", attr.value)
]))
if shape_type == "boxes":
dumper.close_box()
elif shape_type == "polygons":
dumper.close_polygon()
elif shape_type == "polylines":
dumper.close_polyline()
else:
dumper.close_points()
dumper.close_image()
else:
paths = {}
paths["boxes"] = self.to_box_paths()
paths["polygons"] = self.to_polygon_paths()
paths["polylines"] = self.to_polyline_paths()
paths["points"] = self.to_points_paths()
im_w = im_meta_data['original_size'][0]['width']
im_h = im_meta_data['original_size'][0]['height']
for shape_type in ["boxes", "polygons", "polylines", "points"]:
path_list = paths[shape_type]
for path in path_list:
dump_dict = OrderedDict([
("id", str(path.client_id)),
("label", path.label.name),
])
if path.group_id:
dump_dict['group_id'] = str(path.group_id)
dumper.open_track(dump_dict)
if shape_type == "boxes":
for box in path.get_interpolated_boxes():
if db_task.flipped:
_flip_box(box, im_w, im_h)
dump_dict = OrderedDict([
("frame", str(box.frame)),
("xtl", "{:.2f}".format(box.xtl)),
("ytl", "{:.2f}".format(box.ytl)),
("xbr", "{:.2f}".format(box.xbr)),
("ybr", "{:.2f}".format(box.ybr)),
("outside", str(int(box.outside))),
("occluded", str(int(box.occluded))),
("keyframe", str(int(box.keyframe)))
])
if db_task.z_order:
dump_dict["z_order"] = str(box.z_order)
dumper.open_box(dump_dict)
for attr in path.attributes + box.attributes:
dumper.add_attribute(OrderedDict([
("name", attr.name),
("value", attr.value)
]))
dumper.close_box()
else:
for shape in path.get_interpolated_shapes():
if db_task.flipped:
_flip_shape(shape, im_w, im_h)
dump_dict = OrderedDict([
("frame", str(shape.frame)),
("points", ';'.join((
','.join((
"{:.2f}".format(float(p.split(',')[0])),
"{:.2f}".format(float(p.split(',')[1]))
)) for p in shape.points.split(' '))
)),
("outside", str(int(shape.outside))),
("occluded", str(int(shape.occluded))),
("keyframe", str(int(shape.keyframe)))
])
if db_task.z_order:
dump_dict["z_order"] = str(shape.z_order)
if shape_type == "polygons":
dumper.open_polygon(dump_dict)
elif shape_type == "polylines":
dumper.open_polyline(dump_dict)
else:
dumper.open_points(dump_dict)
for attr in path.attributes + shape.attributes:
dumper.add_attribute(OrderedDict([
("name", attr.name),
("value", attr.value)
]))
if shape_type == "polygons":
dumper.close_polygon()
elif shape_type == "polylines":
dumper.close_polyline()
else:
dumper.close_points()
dumper.close_track()
dumper.close_root()