From 73246ed74c6e9312d03b6fa018f204088d931326 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 19 Sep 2018 15:44:50 +0300 Subject: [PATCH 01/12] improved logic of synchronization with server --- cvat/apps/engine/annotation.py | 591 ++++++++++++++---- .../engine/static/engine/js/annotationUI.js | 11 +- .../static/engine/js/shapeCollection.js | 252 ++++++-- .../engine/static/engine/js/shapeCreator.js | 2 + .../engine/static/engine/js/shapeGrouper.js | 2 +- cvat/apps/engine/static/engine/js/shapes.js | 107 +++- .../engine/static/engine/js/userConfig.js | 2 +- cvat/apps/engine/views.py | 5 +- cvat/requirements/development.txt | 2 +- 9 files changed, 777 insertions(+), 197 deletions(-) diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index f6c283767a6a..eac1e456092f 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -11,7 +11,6 @@ from scipy.optimize import linear_sum_assignment from collections import OrderedDict from distutils.util import strtobool -from xml.dom import minidom from xml.sax.saxutils import XMLGenerator from abc import ABCMeta, abstractmethod from PIL import Image @@ -77,12 +76,21 @@ def save_job(jid, data): Save new annotations for the job. """ db_job = models.Job.objects.select_for_update().get(id=jid) + id_mapping = {} + annotation = _AnnotationForJob(db_job) - annotation.init_from_client(data) - annotation.save_to_db() + if data['pre_erase']: + annotation.delete_objs_from_db() + + for action in ['create', 'update', 'delete']: + annotation.init_from_client(data[action]) + id_mapping[action] = annotation.save_to_db(action) + db_job.segment.task.updated_date = timezone.now() db_job.segment.task.save() + return id_mapping + # pylint: disable=unused-argument def save_task(tid, data): """ @@ -133,13 +141,15 @@ def __init__(self, db_attr, value): self.value = str(value) class _BoundingBox: - def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, attributes=None): + def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, db_id=None, 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.db_id = db_id + self.client_id = client_id self.frame = frame self.attributes = attributes if attributes else [] @@ -156,14 +166,14 @@ 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, attributes=None): - super().__init__(x0, y0, x1, y1, frame, occluded, z_order, attributes) + def __init__(self, label, x0, y0, x1, y1, frame, group_id, occluded, z_order, db_id=None, client_id=None, attributes=None): + super().__init__(x0, y0, x1, y1, frame, occluded, z_order, db_id, 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, attributes) + super().__init__(x0, y0, x1, y1, frame, occluded, z_order, None, None, attributes) self.outside = outside class _InterpolatedBox(_TrackedBox): @@ -172,25 +182,27 @@ def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, outside, keyframe, self.keyframe = keyframe class _PolyShape: - def __init__(self, points, frame, occluded, z_order, attributes=None): + def __init__(self, points, frame, occluded, z_order, db_id=None, client_id=None, attributes=None): self.points = points + self.frame = frame self.occluded = occluded self.z_order = z_order - self.frame = frame + self.db_id = db_id + 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, attributes=None): - super().__init__(points, frame, occluded, z_order, attributes) + def __init__(self, label, points, frame, group_id, occluded, z_order, db_id=None,client_id=None, attributes=None): + super().__init__(points, frame, occluded, z_order, db_id, 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, attributes) + def __init__(self, points, frame, occluded, z_order, outside, db_id=None, client_id=None, attributes=None): + super().__init__(points, frame, occluded, z_order, db_id, client_id, attributes) self.outside = outside class _InterpolatedPolyShape(_TrackedPolyShape): @@ -199,12 +211,14 @@ def __init__(self, points, frame, occluded, z_order, outside, keyframe, attribut self.keyframe = keyframe class _BoxPath: - def __init__(self, label, start_frame, stop_frame, group_id, boxes=None, attributes=None): + def __init__(self, label, start_frame, stop_frame, group_id, boxes=None, db_id=None, client_id=None, attributes=None): self.label = label self.frame = start_frame - self.group_id = group_id self.stop_frame = stop_frame + self.group_id = group_id self.boxes = boxes if boxes else [] + self.db_id = db_id + 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 @@ -273,12 +287,14 @@ def add_attribute(self, attr): self.attributes.append(attr) class _PolyPath: - def __init__(self, label, start_frame, stop_frame, group_id, shapes=None, attributes=None): + def __init__(self, label, start_frame, stop_frame, group_id, shapes=None, db_id=None, client_id=None, attributes=None): self.label = label self.frame = start_frame - self.group_id = group_id self.stop_frame = stop_frame + self.group_id = group_id self.shapes = shapes if shapes else [] + self.db_id = db_id + self.client_id = client_id self.attributes = attributes if attributes else [] self._interpolated_shapes = [] # ??? @@ -339,8 +355,16 @@ def to_boxes(self): for path in self.box_paths: for box in path.get_interpolated_boxes(): if not box.outside: - box = _LabeledBox(path.label, box.xtl, box.ytl, box.xbr, box.ybr, - box.frame, path.group_id, box.occluded, box.z_order, box.attributes + path.attributes) + box = _LabeledBox( + label=path.label, + x0=box.xtl, y0=box.ytl, x1=box.xbr, y1=box.ybr, + frame=box.frame, + groupd_id=path.group_id, + occluded=box.occluded, + z_order=box.z_order, + db_id=db_id, + attributes=box.attributes + path.attributes, + ) boxes.append(box) return self.boxes + boxes @@ -350,8 +374,16 @@ def _to_poly_shapes(self, iter_attr_name): for path in getattr(self, iter_attr_name): for shape in path.get_interpolated_shapes(): if not shape.outside: - shape = _LabeledPolyShape(path.label, shape.points, shape.frame, path.group_id, - shape.occluded, shape.z_order, shape.attributes + path.attributes) + shape = _LabeledPolyShape( + label=path.label, + points=shape.points, + frame=shape.frame, + groupd_id=path.group_id, + occluded=shape.occluded, + z_order=shape.z_order, + db_id=shape.db_id, + attributes=shape.attributes + path.attributes, + ) shapes.append(shape) return shapes @@ -372,7 +404,15 @@ def to_box_paths(self): box1 = copy.copy(box0) box1.outside = True box1.frame += 1 - path = _BoxPath(box.label, box.frame, box.frame + 1, box.group_id, [box0, box1], box.attributes) + path = _BoxPath( + label=box.label, + start_frame=box.frame, + stop_frame=box.frame + 1, + group_id=box.group_id, + boxes=[box0, box1], + db_id=box.db_id, + attributes=box.attributes, + ) paths.append(path) return self.box_paths + paths @@ -385,7 +425,15 @@ def _to_poly_paths(self, iter_attr_name): shape1 = copy.copy(shape0) shape1.outside = True shape1.frame += 1 - path = _PolyPath(shape.label, shape.frame, shape.frame + 1, shape.group_id, [shape0, shape1], shape.attributes) + path = _PolyPath( + label=shape.label, + start_frame=shape.frame, + stop_frame=shape.frame + 1, + group_id=shape.group_id, + shapes=[shape0, shape1], + db_id=shape.bd_id, + attributes=shape.attributes, + ) paths.append(path) return paths @@ -450,11 +498,16 @@ class dotdict(OrderedDict): 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 = max(min(xtl, im_size['width']), 0) - ytl = max(min(ytl, im_size['height']), 0) - xbr = max(min(xbr, im_size['width']), 0) - ybr = max(min(ybr, im_size['height']), 0) + 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): @@ -463,76 +516,92 @@ def _clamp_poly(self, points, im_size): for p in points: p = p.split(',') verified.append('{},{}'.format( - max(min(float(p[0]), im_size['width']), 0), - max(min(float(p[1]), im_size['height']), 0) + 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', - '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', - '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', - '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', - 'labeledpointsattributeval__value', 'labeledpointsattributeval__spec_id', - 'labeledpointsattributeval__id'), { - 'attributes': [ - 'labeledpointsattributeval__value', - 'labeledpointsattributeval__spec_id', - 'labeledpointsattributeval__id' - ] - }, 'labeledpointsattributeval_set' - ] + @staticmethod + def _get_values(shape_type): + if shape_type == 'polygons': + return [ + ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', + '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', + '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', + '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', + 'labeledpointsattributeval__value', 'labeledpointsattributeval__spec_id', + 'labeledpointsattributeval__id'), { + 'attributes': [ + 'labeledpointsattributeval__value', + 'labeledpointsattributeval__spec_id', + 'labeledpointsattributeval__id' + ] + }, 'labeledpointsattributeval_set' + ] + + def init_from_db(self): self.reset() for shape_type in ['boxes', 'points', 'polygons', 'polylines']: - (values, merge_keys, prefetch) = get_values(shape_type) + (values, merge_keys, prefetch) = self._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, db_shape.xtl, db_shape.ytl, db_shape.xbr, db_shape.ybr, - db_shape.frame, db_shape.group_id, db_shape.occluded, db_shape.z_order) + 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, + db_id=db_shape.id, + ) else: - shape = _LabeledPolyShape(label, db_shape.points, db_shape.frame, - db_shape.group_id, db_shape.occluded, db_shape.z_order) + 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, + db_id=db_shape.id, + ) for db_attr in db_shape.attributes: if db_attr.id != None: spec = self.db_attributes[db_attr.spec_id] @@ -667,7 +736,13 @@ def get_values(shape_type): 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, db_path.frame, self.stop_frame, db_path.group_id) + path = _BoxPath( + label=label, + start_frame=db_path.frame, + stop_frame=self.stop_frame, + group_id=db_path.group_id, + db_id=db_path.id, + ) for db_attr in db_path.attributes: spec = self.db_attributes[db_attr.spec_id] attr = _Attribute(spec, db_attr.value) @@ -675,8 +750,13 @@ def get_values(shape_type): frame = -1 for db_shape in db_path.shapes: - box = _TrackedBox(db_shape.xtl, db_shape.ytl, db_shape.xbr, db_shape.ybr, - db_shape.frame, db_shape.occluded, db_shape.z_order, db_shape.outside) + 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 @@ -695,7 +775,7 @@ def get_values(shape_type): 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, db_path.frame, self.stop_frame, db_path.group_id) + path = _PolyPath(label, db_path.frame, self.stop_frame, db_path.group_id, db_path.id) for db_attr in db_path.attributes: spec = self.db_attributes[db_attr.spec_id] attr = _Attribute(spec, db_attr.value) @@ -703,7 +783,13 @@ def get_values(shape_type): frame = -1 for db_shape in db_path.shapes: - shape = _TrackedPolyShape(db_shape.points, db_shape.frame, db_shape.occluded, db_shape.z_order, db_shape.outside) + 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 @@ -731,8 +817,17 @@ def init_from_client(self, data): 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, xtl, ytl, xbr, ybr, int(box['frame']), - int(box['group_id']), strtobool(str(box['occluded'])), int(box['z_order'])) + + 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']), + db_id=int(box['db_id']) if 'db_id' in box and box['db_id'] else None, + client_id=int(box['client_id']), + ) for attr in box['attributes']: spec = self.db_attributes[int(attr['id'])] @@ -747,8 +842,16 @@ def init_from_client(self, data): 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, points, int(poly_shape['frame']), - int(poly_shape['group_id']), poly_shape['occluded'], int(poly_shape['z_order'])) + 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']), + db_id=int(poly_shape['db_id']) if 'db_id' in poly_shape and poly_shape['db_id'] else None, + client_id=int(poly_shape['client_id']), + ) for attr in poly_shape['attributes']: spec = self.db_attributes[int(attr['id'])] @@ -781,9 +884,14 @@ def init_from_client(self, data): 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(xtl, ytl, xbr, ybr, int(box['frame']), strtobool(str(box['occluded'])), - int(box['z_order']), strtobool(str(box['outside']))) - assert tracked_box.frame > frame + 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']: @@ -805,8 +913,15 @@ def init_from_client(self, data): attributes.append(attr) assert frame <= self.stop_frame - box_path = _BoxPath(label, min(list(map(lambda box: box.frame, boxes))), self.stop_frame, - int(path['group_id']), boxes, attributes) + 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, + db_id=int(path['db_id']) if 'db_id' in path and path['db_id'] else None, + client_id=int(path['client_id']), + attributes=attributes, + ) self.box_paths.append(box_path) for poly_path_type in ['points_paths', 'polygon_paths', 'polyline_paths']: @@ -833,8 +948,13 @@ def init_from_client(self, data): 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, int(poly_shape['frame']), strtobool(str(poly_shape['occluded'])), - int(poly_shape['z_order']), strtobool(str(poly_shape['outside']))) + 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 @@ -856,8 +976,16 @@ def init_from_client(self, data): attr = _Attribute(spec, str(attr['value'])) attributes.append(attr) - poly_path = _PolyPath(label, min(list(map(lambda shape: shape.frame, poly_shapes))), self.stop_frame + 1, - int(path['group_id']), poly_shapes, attributes) + 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, + db_id=int(path['db_id']) if 'db_id' in path and path['db_id'] else None, + client_id=int(path['client_id']), + attributes=attributes, + ) getattr(self, poly_path_type).append(poly_path) @@ -898,7 +1026,7 @@ def _get_shape_attr_class(self, shape_type): return models.TrackedPointsAttributeVal def _save_paths_to_db(self): - self.db_job.objectpath_set.all().delete() + id_mapping = {} for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: db_paths = [] @@ -906,7 +1034,8 @@ def _save_paths_to_db(self): db_shapes = [] db_shape_attrvals = [] - for path in getattr(self, shape_type): + 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] @@ -929,8 +1058,8 @@ def _save_paths_to_db(self): db_attrval.value = attr.value db_path_attrvals.append(db_attrval) - shapes = path.boxes if hasattr(path, 'boxes') else path.shapes - for shape in shapes: + 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': @@ -978,6 +1107,8 @@ def _save_paths_to_db(self): elif shape_type == 'points_paths': db_paths = list(self.db_job.objectpath_set.filter(shapes="points")) + id_mapping[shape_type] = {client_shape.client_id: db_shape.id for client_shape, db_shape in zip(shapes, db_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) @@ -1006,6 +1137,8 @@ def _save_paths_to_db(self): self._get_shape_attr_class(shape_type).objects.bulk_create(db_shape_attrvals) + return id_mapping + def _get_shape_set(self, shape_type): if shape_type == 'polygons': return self.db_job.labeledpolygon_set @@ -1020,12 +1153,14 @@ def _save_shapes_to_db(self): db_shapes = [] db_attrvals = [] + id_mapping = {} + for shape_type in ['polygons', 'polylines', 'points', 'boxes']: - self._get_shape_set(shape_type).all().delete() db_shapes = [] db_attrvals = [] - for shape in getattr(self, shape_type): + 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] @@ -1067,6 +1202,8 @@ def _save_shapes_to_db(self): # are auto incremented. Thus we will not be inside the 'if'. db_shapes = list(self._get_shape_set(shape_type).all()) + id_mapping[shape_type] = {client_shape.client_id: db_shape.id for client_shape, db_shape in zip(shapes, db_shapes) } + for db_attrval in db_attrvals: if shape_type == 'polygons': db_attrval.polygon_id = db_shapes[db_attrval.polygon_id].id @@ -1078,10 +1215,227 @@ def _save_shapes_to_db(self): db_attrval.points_id = db_shapes[db_attrval.points_id].id self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) + return id_mapping + + def _get_relaited_obj_field_name(self, shape_type): + 'polygon_paths', 'polyline_paths', 'points_paths', 'box_paths' + if shape_type == 'boxes' or shape_type == 'box_paths': + return 'box' + elif shape_type == 'points' or shape_type == 'points_paths': + return 'points' + elif shape_type == 'polygons' or shape_type == 'polygon_paths': + return 'polygon' + elif shape_type == 'polylines' or shape_type == 'polyline_paths': + return 'polyline' + + def _update_shapes_to_db(self): + updated_ids = {} + for shape_type in ['boxes', 'points', 'polygons', 'polylines']: + db_attrvals = [] + updated_ids[shape_type] = [] + shapes_to_update = {shape.db_id: shape for shape in getattr(self, shape_type)} + if not shapes_to_update: + continue + + db_ids_to_update = list(shapes_to_update.keys()) + + attr_filter = {'{}__id__in'.format(self._get_relaited_obj_field_name(shape_type)):db_ids_to_update} + self._get_shape_attr_class(shape_type).objects.filter(**attr_filter).delete() + + for shape_id_to_update, shape_to_update in shapes_to_update.items(): + if shape_type == 'boxes': + self._get_shape_class(shape_type).objects.filter(id=shape_id_to_update).update( + label = shape_to_update.label.id, + group_id = shape_to_update.group_id, + xtl = shape_to_update.xtl, + ytl = shape_to_update.ytl, + xbr = shape_to_update.xbr, + ybr = shape_to_update.ybr, + ) + else: + self._get_shape_class(shape_type).objects.filter(id=shape_id_to_update).update( + label = shape_to_update.label.id, + group_id = shape_to_update.group_id, + points = shape_to_update.points + ) + + for attr in shape_to_update.attributes: + db_attrval = self._get_shape_attr_class(shape_type)() + if shape_type == 'polygons': + db_attrval.polygon_id = shape_id_to_update + elif shape_type == 'polylines': + db_attrval.polyline_id = shape_id_to_update + elif shape_type == 'boxes': + db_attrval.box_id = shape_id_to_update + else: + db_attrval.points_id = shape_id_to_update + + db_attrval.spec = self.db_attributes[attr.id] + db_attrval.value = attr.value + db_attrvals.append(db_attrval) + + self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) + + updated_client_ids = list(shape.client_id for shape in shapes_to_update.values()) + updated_ids[shape_type] = updated_client_ids + + return updated_ids + + def _update_paths_to_db(self): + updated_ids = {} + for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: + db_path_attrvals = [] + db_shapes = [] + db_shape_attrvals = [] + updated_ids[shape_type] = [] + + shapes_to_update = {shape.db_id: shape for shape in getattr(self, shape_type)} + if not shapes_to_update: + continue + + db_ids_to_update = list(shapes_to_update.keys()) + + if None in db_ids_to_update: + raise Exception('Trying to update None id') + + #delete shape attributes + models.ObjectPathAttributeVal.objects.filter(track__id__in=db_ids_to_update).delete() + + # delete all relaited paths with path_attributes + shape_class = self._get_shape_class(shape_type) + shape_class.objects.filter(track__id__in=db_ids_to_update).delete() + + # update shape props + for shape_id_to_update, shape_to_update in shapes_to_update.items(): + models.ObjectPath.objects.filter(id=shape_id_to_update).update( + label = shape_to_update.label.id, + frame = shape_to_update.frame, + group_id = shape_to_update.group_id, + ) + + for attr in shape_to_update.attributes: + db_attrspec = self.db_attributes[attr.id] + db_attrval = models.ObjectPathAttributeVal() + db_attrval.track_id = shape_id_to_update + db_attrval.spec = db_attrspec + db_attrval.value = attr.value + db_path_attrvals.append(db_attrval) + + path_shapes = shape_to_update.boxes if hasattr(shape_to_update, 'boxes') else shape_to_update.shapes + for shape in path_shapes: + db_shape = shape_class() + db_shape.track_id = shape_id_to_update + 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) + + models.ObjectPathAttributeVal.objects.bulk_create(db_path_attrvals) + + 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.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) + + updated_client_ids = list(shape.client_id for shape in shapes_to_update.values()) + updated_ids[shape_type] = updated_client_ids + + return updated_ids + + def _delete_shapes_from_db(self): + deleted_ids = {} + for shape_type in ['polygons', 'polylines', 'points', 'boxes']: + db_ids_to_delete = list(shape.db_id for shape in getattr(self, shape_type)) + deleted = self._get_shape_set(shape_type).filter(id__in=db_ids_to_delete).delete() + class_name = 'engine.{}'.format(self._get_shape_class(shape_type).__name__) + if not (deleted[0] == 0 and len(db_ids_to_delete) == 0) or (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): + raise Exception('Number of deleted object doesn\'t match with requested number') + + deleted_client_ids = list(shape.client_id for shape in getattr(self, shape_type)) + deleted_ids[shape_type] = deleted_client_ids + + return deleted_ids + + def _delete_paths_from_db(self): + deleted_ids = {} + for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: + db_ids_to_delete = list(shape.db_id for shape in getattr(self, shape_type)) + deleted = self.db_job.objectpath_set.filter(id__in=db_ids_to_delete).delete() + class_name = 'engine.ObjectPath' + if not (deleted[0] == 0 and len(db_ids_to_delete) == 0) and \ + (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): + raise Exception('Number of deleted object doesn\'t match with requested number') + + + deleted_client_ids = list(shape.client_id for shape in getattr(self, shape_type)) + deleted_ids[shape_type] = deleted_client_ids + + return deleted_ids + + 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 save_to_db(self, action): + if action == 'create': + shape_mapping = self._save_shapes_to_db() + path_mapping = self._save_paths_to_db() + elif action == 'update': + shape_mapping = self._update_shapes_to_db() + path_mapping = self._update_paths_to_db() + elif action == 'delete': + shape_mapping = self._delete_shapes_from_db() + path_mapping = self._delete_paths_from_db() + + return {**shape_mapping, **path_mapping} - def save_to_db(self): - self._save_shapes_to_db() - self._save_paths_to_db() + def delete_objs_from_db(self): + self._delete_all_shapes_from_db() + self._delete_all_paths_from_db() def to_client(self): data = { @@ -1097,6 +1451,7 @@ def to_client(self): for box in self.boxes: data["boxes"].append({ + "db_id": box.db_id, "label_id": box.label.id, "group_id": box.group_id, "xtl": box.xtl, @@ -1112,6 +1467,7 @@ def to_client(self): for poly_type in ['polygons', 'polylines', 'points']: for poly in getattr(self, poly_type): data[poly_type].append({ + "db_id": poly.db_id, "label_id": poly.label.id, "group_id": poly.group_id, "points": poly.points, @@ -1123,12 +1479,14 @@ def to_client(self): for box_path in self.box_paths: data["box_paths"].append({ + "db_id": box_path.db_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: ({ + "db_id": box.db_id, "frame": box.frame, "xtl": box.xtl, "ytl": box.ytl, @@ -1145,12 +1503,14 @@ def to_client(self): 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({ + "db_id": poly_path.db_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: ({ + "db_id": shape.db_id, "frame": shape.frame, "points": shape.points, "occluded": shape.occluded, @@ -1707,7 +2067,8 @@ def _flip_shape(shape, im_w, im_h): ("ytl", "{:.2f}".format(shape.ytl)), ("xbr", "{:.2f}".format(shape.xbr)), ("ybr", "{:.2f}".format(shape.ybr)), - ("occluded", str(int(shape.occluded))) + ("occluded", str(int(shape.occluded))), + ("db_id", "{}".format(shape.db_id)), ]) if db_task.z_order: dump_dict['z_order'] = str(shape.z_order) @@ -1726,7 +2087,8 @@ def _flip_shape(shape, im_w, im_h): "{:.2f}".format(float(p.split(',')[1])) )) for p in shape.points.split(' ')) )), - ("occluded", str(int(shape.occluded))) + ("occluded", str(int(shape.occluded))), + ("db_id", "{}".format(shape.db_id)), ]) if db_task.z_order: @@ -1773,7 +2135,8 @@ def _flip_shape(shape, im_w, im_h): for path in path_list: dump_dict = OrderedDict([ ("id", str(path_idx)), - ("label", path.label.name) + ("label", path.label.name), + ("db_id", "{}".format(path.db_id)), ]) if path.group_id: dump_dict['group_id'] = str(path.group_id) diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index f0c5fab497f0..9ac43ab36f62 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -571,7 +571,7 @@ function uploadAnnotation(shapeCollectionModel, historyModel, annotationParser, try { historyModel.empty(); shapeCollectionModel.empty(); - shapeCollectionModel.import(data); + shapeCollectionModel.import(data, ShapeState.create); shapeCollectionModel.update(); } finally { @@ -609,8 +609,9 @@ function saveAnnotation(shapeCollectionModel, job) { 'points count': totalStat.points.annotation + totalStat.points.interpolation, }); - let exportedData = shapeCollectionModel.export(); - let annotationLogs = Logger.getLogs(); + const exportedData = shapeCollectionModel.export(); + shapeCollectionModel.reset_state(); + const annotationLogs = Logger.getLogs(); const data = { annotation: exportedData, @@ -620,8 +621,9 @@ function saveAnnotation(shapeCollectionModel, job) { saveButton.prop('disabled', true); saveButton.text('Saving..'); - saveJobRequest(job.jobid, data, () => { + saveJobRequest(job.jobid, data, (response) => { // success + shapeCollectionModel.syncWithDB(response, true); shapeCollectionModel.updateHash(); saveButton.text('Success!'); setTimeout(() => { @@ -630,6 +632,7 @@ function saveAnnotation(shapeCollectionModel, job) { }, 3000); }, (response) => { // error + shapeCollectionModel.syncWithDB(response, false); saveButton.prop('disabled', false); saveButton.text('Save Work'); let message = `Impossible to save job. Errors was occured. Status: ${response.status}`; diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 67cbc9ee4585..f3e15550ce4d 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -51,6 +51,8 @@ class ShapeCollectionModel extends Listener { this._colorIdx = 0; this._filter = new FilterModel(() => this.update()); this._splitter = new ShapeSplitter(); + this._saved_state = {}; + this._erased = false; } _nextIdx() { @@ -184,83 +186,234 @@ class ShapeCollectionModel extends Listener { } } - import(data) { + import(data, initialState = ShapeState.nothing) { for (let box of data.boxes) { - this.add(box, 'annotation_box'); + this.add(box, 'annotation_box', initialState); } for (let box_path of data.box_paths) { - this.add(box_path, 'interpolation_box'); + this.add(box_path, 'interpolation_box', initialState); } for (let points of data.points) { - this.add(points, 'annotation_points'); + this.add(points, 'annotation_points', initialState); } for (let points_path of data.points_paths) { - this.add(points_path, 'interpolation_points'); + this.add(points_path, 'interpolation_points', initialState); } for (let polygon of data.polygons) { - this.add(polygon, 'annotation_polygon'); + this.add(polygon, 'annotation_polygon', initialState); } for (let polygon_path of data.polygon_paths) { - this.add(polygon_path, 'interpolation_polygon'); + this.add(polygon_path, 'interpolation_polygon', initialState); } for (let polyline of data.polylines) { - this.add(polyline, 'annotation_polyline'); + this.add(polyline, 'annotation_polyline', initialState); } for (let polyline_path of data.polyline_paths) { - this.add(polyline_path, 'interpolation_polyline'); + this.add(polyline_path, 'interpolation_polyline', initialState); } this.notify(); return this; } + _createExportContainer() { + const container = {}; + ['create', 'update', 'delete'].forEach( action => { + container[action] = { + "boxes": [], + "box_paths": [], + "points": [], + "points_paths": [], + "polygons": [], + "polygon_paths": [], + "polylines": [], + "polyline_paths": [], + }; + }); + container.pre_erase = this._erased; + + return container; + } + + _getExportTargetContainer(export_type, shape_type, container) { + let shape_container_target = undefined; + let export_action_container = undefined; + + if (export_type === ShapeState.create) { + export_action_container = container.create; + } else if (export_type === ShapeState.update) { + export_action_container = container.update; + } else if (export_type === ShapeState.delete) { + export_action_container = container.delete; + } + + if (!export_action_container) { + throw Error('Undefined action of shape'); + } + + switch (shape_type) { + case 'annotation_box': + shape_container_target = export_action_container.boxes; + break; + case 'interpolation_box': + shape_container_target = export_action_container.box_paths; + break; + case 'annotation_points': + shape_container_target = export_action_container.points; + break; + case 'interpolation_points': + shape_container_target = export_action_container.points_paths; + break; + case 'annotation_polygon': + shape_container_target = export_action_container.polygons; + break; + case 'interpolation_polygon': + shape_container_target = export_action_container.polygon_paths; + break; + case 'annotation_polyline': + shape_container_target = export_action_container.polylines; + break; + case 'interpolation_polyline': + shape_container_target = export_action_container.polyline_paths; + } + + if (!shape_container_target) { + throw Error('Undefined shape type'); + } + return shape_container_target; + } + + _updateShapeDbIds(response) { + /* + Function updates db ids for successfully saved objects and makes simple sanity checks. + */ + for (const shape_type in response['create']) { + const shapes = response['create'][shape_type]; + const saved_states = this._saved_state['create'][shape_type]; + for (const shape_client_id in shapes) { + //check saved state + if (!saved_states.includes(+shape_client_id)) { + throw Error('Unexpected behaviour: cretaed object has unexpected client id'); + } + // current state cannot be created + if (this._shapes[shape_client_id] === ShapeState.create) { + throw Error('Unexpected behaviour: unexpected current state for created object'); + } + this._shapes[shape_client_id]._dbId = shapes[shape_client_id]; + } + } + + for (const shape_type in response['update']) { + const shapes = response['update'][shape_type]; + const saved_states = this._saved_state['update'][shape_type]; + for (const shape_client_id of shapes) { + //check saved state + if (!saved_states.includes(+shape_client_id)) { + throw Error('Unexpected behaviour: updated object has unexpected client id'); + } + // current state cannot be created + if (this._shapes[shape_client_id] === ShapeState.create) { + throw Error('Unexpected behaviour: unexpected current state for updated object'); + } + } + } + + for (const shape_type in response['delete']) { + const shapes = response['delete'][shape_type]; + const saved_states = this._saved_state['delete'][shape_type]; + for (const shape_client_id of shapes) { + //check saved state + if (!saved_states.includes(+shape_client_id)) { + throw Error('Unexpected behaviour: deleted object has unexpected client id'); + } + + if (this._shapes[shape_client_id] === ShapeState.created) { + throw Error('Unexpected behaviour: unexpected current state for deleted object'); + } + // TODO: check need of this condition + if (this._shapes[shape_client_id] === ShapeState.delete) { + this._shapes[shape_client_id].state = ShapeState.nothing; + } + // object was deleted and has no db id + this._shapes[shape_client_id]._dbId = null; + } + } + } + + _revertUnsavedStates() { + /* + Function reverts shape export states in case of failed save request + */ + for (const shape_type in this._saved_state['create']) { + for (const shape_client_id of this._saved_state['create'][shape_type]) { + const currentState = this._shapes[shape_client_id].state; + if (currentState === ShapeState.delete) { + this._shapes[shape_client_id].state = ShapeState.nothing; + } else { + this._shapes[shape_client_id].state = ShapeState.create; + } + } + } + + for (const shape_type in this._saved_state['update']) { + for (const shape_client_id of this._saved_state['update'][shape_type]) { + const currentState = this._shapes[shape_client_id].state; + if (currentState === ShapeState.create) { + throw Error('Unexpected behaviour: unexpected current state for object that should be updated'); + } else if (currentState === ShapeState.nothing) { + this._shapes[shape_client_id].state = ShapeState.update; + } + } + } + + for (const shape_type in this._saved_state['delete']) { + for (const shape_client_id of this._saved_state['delete'][shape_type]) { + const currentState = this._shapes[shape_client_id].state; + if (currentState === ShapeState.create) { + throw Error('Unexpected behaviour: unexpected current state for object that should be deleted'); + } else if (currentState === ShapeState.nothing) { + this._shapes[shape_client_id].state = ShapeState.delete; + } + } + } + } + + syncWithDB(response, isSuccess) { + if (isSuccess) { + this._updateShapeDbIds(response); + } else { + this._revertUnsavedStates(); + } + } + + reset_state() { + for (const shape of this._shapes) { + shape.state = ShapeState.nothing; + } + this._erased = false; + } export() { - let response = { - "boxes": [], - "box_paths": [], - "points": [], - "points_paths": [], - "polygons": [], - "polygon_paths": [], - "polylines": [], - "polyline_paths": [], - }; + const response = this._createExportContainer(); + this._saved_state = this._createExportContainer(); - for (let shape of this._shapes) { - if (shape.removed) continue; - switch (shape.type) { - case 'annotation_box': - response.boxes.push(shape.export()); - break; - case 'interpolation_box': - response.box_paths.push(shape.export()); - break; - case 'annotation_points': - response.points.push(shape.export()); - break; - case 'interpolation_points': - response.points_paths.push(shape.export()); - break; - case 'annotation_polygon': - response.polygons.push(shape.export()); - break; - case 'interpolation_polygon': - response.polygon_paths.push(shape.export()); - break; - case 'annotation_polyline': - response.polylines.push(shape.export()); - break; - case 'interpolation_polyline': - response.polyline_paths.push(shape.export()); + for (const shape of this._shapes) { + if (shape.state === ShapeState.nothing) { + continue; } + + const target_export_container = this._getExportTargetContainer(shape.state, shape.type, response); + target_export_container.push(shape.export()); + + const saved_state_container_target = this._getExportTargetContainer(shape.state, shape.type, this._saved_state); + saved_state_container_target.push(shape.id); } return JSON.stringify(response); @@ -336,11 +489,12 @@ class ShapeCollectionModel extends Listener { this._shapes = []; this._idx = 0; this._colorIdx = 0; + this._erased = true; this._interpolate(); } - add(data, type) { - let model = buildShapeModel(data, type, this._nextIdx(), this.nextColor()); + add(data, type, initialState=ShapeState.create) { + let model = buildShapeModel(data, type, this._nextIdx(), this.nextColor(), initialState); if (type.startsWith('interpolation')) { this._interpolationShapes.push(model); } @@ -801,6 +955,10 @@ class ShapeCollectionModel extends Listener { get shapes() { return this._shapes; } + + get erased() { + return this._erased; + } } class ShapeCollectionController { diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js index eebcb878c217..a24933ba07be 100644 --- a/cvat/apps/engine/static/engine/js/shapeCreator.js +++ b/cvat/apps/engine/static/engine/js/shapeCreator.js @@ -56,10 +56,12 @@ class ShapeCreatorModel extends Listener { // Undo/redo code window.cvat.addAction('Draw Object', () => { model.removed = true; + model.state = model._dbId ? ShapeState.delete : ShapeState.nothing; model.unsubscribe(this._shapeCollection); }, () => { model.subscribe(this._shapeCollection); model.removed = false; + model.state = ShapeState.create; }, window.cvat.player.frames.current); // End of undo/redo code diff --git a/cvat/apps/engine/static/engine/js/shapeGrouper.js b/cvat/apps/engine/static/engine/js/shapeGrouper.js index deb264df88bd..e37e1f84b788 100644 --- a/cvat/apps/engine/static/engine/js/shapeGrouper.js +++ b/cvat/apps/engine/static/engine/js/shapeGrouper.js @@ -270,4 +270,4 @@ class ShapeGrouperView { } } } -} \ No newline at end of file +} diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 5f85904a12e1..0914b54a22e9 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -14,8 +14,16 @@ const AREA_TRESHOLD = 9; const TEXT_MARGIN = 10; /******************************** SHAPE MODELS ********************************/ + +const ShapeState = Object.freeze({ + 'nothing': 0, + 'create': 1, + 'update': 2, + 'delete': 3 +}); + class ShapeModel extends Listener { - constructor(data, positions, type, id, color) { + constructor(data, positions, type, id, color, exportState) { super('onShapeUpdate', () => this ); this._id = id; this._groupId = data.group_id; @@ -37,6 +45,8 @@ class ShapeModel extends Listener { this._hiddenText = true; this._updateReason = null; this._importAttributes(data.attributes, positions); + this._dbId = data.db_id ? data.db_id : null; + this._exportState = exportState; } _importAttributes(attributes, positions) { @@ -229,6 +239,9 @@ class ShapeModel extends Listener { window.cvat.addAction('Change Attribute', () => { if (typeof(oldAttr) === 'undefined') { delete this._attributes.mutable[frame][attrId]; + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } this._updateReason = 'attributes'; this.notify(); } @@ -249,6 +262,10 @@ class ShapeModel extends Listener { this._attributes.immutable[attrId] = labelsInfo.strToValues(attrInfo.type, value)[0]; } + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } + this._updateReason = 'attributes'; this.notify(); } @@ -263,6 +280,9 @@ class ShapeModel extends Listener { this._label = +labelId; this._importAttributes([], []); this._setupKeyFrames(); + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } this._updateReason = 'changelabel'; this.notify(); } @@ -297,6 +317,9 @@ class ShapeModel extends Listener { // End of undo/redo code this.updatePosition(frame, position, true); + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } this._updateReason = 'occluded'; this.notify(); } @@ -347,6 +370,9 @@ class ShapeModel extends Listener { } this._updateReason = 'outside'; + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } this.notify(); } else { @@ -360,6 +386,9 @@ class ShapeModel extends Listener { let position = this._interpolatePosition(frame); position.outside = !position.outside; this.updatePosition(frame, position, true); + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } // Update the start frame if need and redestribute attributes if (frame < this._frame) { @@ -411,6 +440,9 @@ class ShapeModel extends Listener { this._frame = frame; } } + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } this._updateReason = 'keyframe'; this.notify(); } @@ -464,14 +496,19 @@ class ShapeModel extends Listener { }); this.removed = true; + const newState = this.state === ShapeState.create ? ShapeState.nothing : ShapeState.delete; // Undo/redo code window.cvat.addAction('Remove Object', () => { this.removed = false; + this.state = this._dbId ? ShapeState.update : ShapeState.create; }, () => { this.removed = true; + this.state = newState; + }, window.cvat.player.frames.current); // End of undo/redo code + this.state = newState; } set z_order(value) { @@ -481,6 +518,7 @@ class ShapeModel extends Listener { position.z_order = value; this.updatePosition(frame, position, true); this._updateReason = 'z_order'; + this.state = ShapeState.update; this.notify(); } } @@ -494,6 +532,14 @@ class ShapeModel extends Listener { this.notify(); } + set state(state) { + this._exportState = state; + } + + get state() { + return this._exportState; + } + get removed() { return this._removed; } @@ -599,8 +645,8 @@ class ShapeModel extends Listener { class BoxModel extends ShapeModel { - constructor(data, type, id, color) { - super(data, data.shapes || [], type, id, color); + constructor(data, type, id, color, state) { + super(data, data.shapes || [], type, id, color, state); this._positions = BoxModel.importPositions.call(this, data.shapes || data); this._setupKeyFrames(); } @@ -683,6 +729,9 @@ class BoxModel extends ShapeModel { window.cvat.addAction('Change Position', () => { if (!Object.keys(oldPos).length) { delete this._positions[frame]; + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } this._updateReason = 'position'; this.notify(); } @@ -703,6 +752,10 @@ class BoxModel extends ShapeModel { outside: position.outside, }); } + + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } } if (!silent) { @@ -763,6 +816,8 @@ class BoxModel extends ShapeModel { } return Object.assign({}, this._positions[this._frame], { + db_id: this._dbId, + client_id: this._id, attributes: immutableAttributes, label_id: this._label, group_id: this._groupId, @@ -771,6 +826,8 @@ class BoxModel extends ShapeModel { } else { let boxPath = { + db_id: this._dbId, + client_id: this._id, label_id: this._label, group_id: this._groupId, frame: this._frame, @@ -862,8 +919,8 @@ class BoxModel extends ShapeModel { } class PolyShapeModel extends ShapeModel { - constructor(data, type, id, color) { - super(data, data.shapes || [], type, id, color); + constructor(data, type, id, color, state) { + super(data, data.shapes || [], type, id, color, state); this._positions = PolyShapeModel.importPositions.call(this, data.shapes || data); this._setupKeyFrames(); } @@ -930,6 +987,9 @@ class PolyShapeModel extends ShapeModel { outside: position.outside, }); } + if (this.state !== ShapeState.create) { + this.state = ShapeState.update; + } } if (!silent) { @@ -958,6 +1018,8 @@ class PolyShapeModel extends ShapeModel { } return Object.assign({}, this._positions[this._frame], { + db_id: this._dbId, + client_id: this._id, attributes: immutableAttributes, label_id: this._label, group_id: this._groupId, @@ -966,6 +1028,8 @@ class PolyShapeModel extends ShapeModel { } else { let polyPath = { + db_id: this._dbId, + client_id: this._id, label_id: this._label, group_id: this._groupId, frame: this._frame, @@ -1019,16 +1083,7 @@ class PolyShapeModel extends ShapeModel { } static convertNumberArrayToString(arrayPoints) { - let serializedPoints = ''; - for (let point of arrayPoints) { - serializedPoints += `${point.x},${point.y} `; - } - let len = serializedPoints.length; - if (len) { - serializedPoints = serializedPoints.substring(0, len - 1); - } - - return serializedPoints; + return arrayPoints.map(point => `${point.x},${point.y}`).join(' '); } static importPositions(positions) { @@ -1079,8 +1134,8 @@ class PolyShapeModel extends ShapeModel { } class PointsModel extends PolyShapeModel { - constructor(data, type, id, color) { - super(data, type, id, color); + constructor(data, type, id, color, state) { + super(data, type, id, color, state); this._minPoints = 1; } @@ -1105,8 +1160,8 @@ class PointsModel extends PolyShapeModel { class PolylineModel extends PolyShapeModel { - constructor(data, type, id, color) { - super(data, type, id, color); + constructor(data, type, id, color, state) { + super(data, type, id, color, state); this._minPoints = 2; } @@ -1141,8 +1196,8 @@ class PolylineModel extends PolyShapeModel { class PolygonModel extends PolyShapeModel { - constructor(data, type, id, color) { - super(data, type, id, color); + constructor(data, type, id, color, state) { + super(data, type, id, color, state); this._minPoints = 3; this._draggable = false; } @@ -3152,20 +3207,20 @@ class PointsView extends PolyShapeView { -function buildShapeModel(data, type, idx, color) { +function buildShapeModel(data, type, idx, color, initialState) { switch (type) { case 'interpolation_box': case 'annotation_box': - return new BoxModel(data, type, idx, color); + return new BoxModel(data, type, idx, color, initialState); case 'interpolation_points': case 'annotation_points': - return new PointsModel(data, type, idx, color); + return new PointsModel(data, type, idx, color, initialState); case 'interpolation_polyline': case 'annotation_polyline': - return new PolylineModel(data, type, idx, color); + return new PolylineModel(data, type, idx, color, initialState); case 'interpolation_polygon': case 'annotation_polygon': - return new PolygonModel(data, type, idx, color); + return new PolygonModel(data, type, idx, color, initialState); } throw Error('Unreacheable code was reached.'); } diff --git a/cvat/apps/engine/static/engine/js/userConfig.js b/cvat/apps/engine/static/engine/js/userConfig.js index 9f1fd0b1e345..08bc4b262812 100644 --- a/cvat/apps/engine/static/engine/js/userConfig.js +++ b/cvat/apps/engine/static/engine/js/userConfig.js @@ -343,4 +343,4 @@ class Config { get settings() { return JSON.parse(JSON.stringify(this._settings)); } -} \ No newline at end of file +} diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index f1edac086645..3c2d425ec77b 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -241,7 +241,7 @@ def save_annotation_for_job(request, jid): job_logger[jid].info("save annotation for {} job".format(jid)) data = json.loads(request.body.decode('utf-8')) if 'annotation' in data: - annotation.save_job(jid, json.loads(data['annotation'])) + mapping = annotation.save_job(jid, json.loads(data['annotation'])) if 'logs' in data: client_log_proxy.push_logs(jid, json.loads(data['logs'])) except RequestException as e: @@ -251,8 +251,7 @@ def save_annotation_for_job(request, jid): job_logger[jid].error("cannot save annotation for job {}".format(jid), exc_info=True) return HttpResponseBadRequest(str(e)) - return HttpResponse() - + return JsonResponse(mapping) @login_required @permission_required(perm=['engine.view_task', 'engine.change_annotation'], raise_exception=True) diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index 108a8ab91e1e..fea2fb3b0160 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -11,4 +11,4 @@ six==1.11.0 wrapt==1.10.11 django-extensions==2.0.6 Werkzeug==0.14.1 -snakeviz==0.4.2 \ No newline at end of file +snakeviz==0.4.2 From 989b7cc26b8a68394f55e8e816494aa589f56d82 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 20 Sep 2018 09:18:27 +0300 Subject: [PATCH 02/12] fixed 'save annotation for the task' feature --- .../static/dashboard/js/dashboard.js | 6 ++- cvat/apps/engine/annotation.py | 45 +++++++++++-------- .../static/engine/js/annotationParser.js | 6 ++- .../static/engine/js/shapeCollection.js | 24 ++-------- cvat/apps/engine/static/engine/js/shapes.js | 26 +++++++++-- cvat/apps/tf_annotation/views.py | 27 ++++++----- 6 files changed, 79 insertions(+), 55 deletions(-) diff --git a/cvat/apps/dashboard/static/dashboard/js/dashboard.js b/cvat/apps/dashboard/static/dashboard/js/dashboard.js index 4a8986668ca4..a6cc4f393a54 100644 --- a/cvat/apps/dashboard/static/dashboard/js/dashboard.js +++ b/cvat/apps/dashboard/static/dashboard/js/dashboard.js @@ -542,11 +542,15 @@ function uploadAnnotationRequest() { return; } + const exportData = createExportContainer(); + exportData.create = parsed; + exportData.pre_erase = true; + let asyncSave = function() { $.ajax({ url: '/save/annotation/task/' + window.cvat.dashboard.taskID, type: 'POST', - data: JSON.stringify(parsed), + data: JSON.stringify(exportData), contentType: 'application/json', success: function() { let message = 'Annotation successfully uploaded'; diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index eac1e456092f..822a79adc1f3 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -105,16 +105,20 @@ def save_task(tid, data): jid = segment.job_set.first().id start = segment.start_frame stop = segment.stop_frame - splitted_data[jid] = { - "boxes": list(filter(lambda x: start <= int(x['frame']) <= stop, data['boxes'])), - "polygons": list(filter(lambda x: start <= int(x['frame']) <= stop, data['polygons'])), - "polylines": list(filter(lambda x: start <= int(x['frame']) <= stop, data['polylines'])), - "points": list(filter(lambda x: start <= int(x['frame']) <= stop, data['points'])), - "box_paths": list(filter(lambda x: len(list(filter(lambda y: (start <= int(y['frame']) <= stop) and (not y['outside']), x['shapes']))), data['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['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['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['points_paths'])), - } + 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'])), + } + + splitted_data[jid]['pre_erase'] = data['pre_erase'] for jid, _data in splitted_data.items(): save_job(jid, _data) @@ -1091,6 +1095,8 @@ def _save_paths_to_db(self): db_shapes.append(db_shape) db_paths.append(db_path) + # in case of sqlite have to store exists ids + db_paths_ids = list(self.db_job.objectpath_set.values_list('id', flat=True)) db_paths = models.ObjectPath.objects.bulk_create(db_paths) if db_paths and db_paths[0].id == None: @@ -1099,13 +1105,13 @@ def _save_paths_to_db(self): # 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.filter(shapes="polygons")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="polygons")) elif shape_type == 'polyline_paths': - db_paths = list(self.db_job.objectpath_set.filter(shapes="polylines")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="polylines")) elif shape_type == 'box_paths': - db_paths = list(self.db_job.objectpath_set.filter(shapes="boxes")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="boxes")) elif shape_type == 'points_paths': - db_paths = list(self.db_job.objectpath_set.filter(shapes="points")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="points")) id_mapping[shape_type] = {client_shape.client_id: db_shape.id for client_shape, db_shape in zip(shapes, db_paths) } @@ -1116,6 +1122,7 @@ def _save_paths_to_db(self): 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: @@ -1123,7 +1130,7 @@ def _save_paths_to_db(self): # 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.filter(track__job_id=self.db_job.id)) + 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': @@ -1193,6 +1200,7 @@ def _save_shapes_to_db(self): db_shapes.append(db_shape) + db_shapes_ids = list(self._get_shape_class(shape_type).objects.filter(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: @@ -1200,7 +1208,7 @@ def _save_shapes_to_db(self): # 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).all()) + db_shapes = list(self._get_shape_set(shape_type).exclude(id__in=db_shapes_ids)) id_mapping[shape_type] = {client_shape.client_id: db_shape.id for client_shape, db_shape in zip(shapes, db_shapes) } @@ -1357,6 +1365,7 @@ def _update_paths_to_db(self): models.ObjectPathAttributeVal.objects.bulk_create(db_path_attrvals) + 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: @@ -1364,7 +1373,7 @@ def _update_paths_to_db(self): # 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.filter(track__job_id=self.db_job.id)) + 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': @@ -1389,7 +1398,7 @@ def _delete_shapes_from_db(self): db_ids_to_delete = list(shape.db_id for shape in getattr(self, shape_type)) deleted = self._get_shape_set(shape_type).filter(id__in=db_ids_to_delete).delete() class_name = 'engine.{}'.format(self._get_shape_class(shape_type).__name__) - if not (deleted[0] == 0 and len(db_ids_to_delete) == 0) or (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): + if not (deleted[0] == 0 and len(db_ids_to_delete) == 0) and (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): raise Exception('Number of deleted object doesn\'t match with requested number') deleted_client_ids = list(shape.client_id for shape in getattr(self, shape_type)) diff --git a/cvat/apps/engine/static/engine/js/annotationParser.js b/cvat/apps/engine/static/engine/js/annotationParser.js index 92536516ec36..c4054745b7e7 100644 --- a/cvat/apps/engine/static/engine/js/annotationParser.js +++ b/cvat/apps/engine/static/engine/js/annotationParser.js @@ -15,6 +15,7 @@ class AnnotationParser { this._flipped = job.flipped; this._im_meta = job.image_meta_data; this._labelsInfo = labelsInfo; + this.counter = 0; } _xmlParseError(parsedXML) { @@ -224,6 +225,7 @@ class AnnotationParser { ybr: ybr, z_order: z_order, attributes: attributeList, + client_id: this.counter++, }); } else { @@ -236,6 +238,7 @@ class AnnotationParser { occluded: occluded, z_order: z_order, attributes: attributeList, + client_id: this.counter++, }); } } @@ -307,7 +310,8 @@ class AnnotationParser { group_id: +groupId, frame: +parsed[type][0].getAttribute('frame'), attributes: [], - shapes: [] + shapes: [], + client_id: this.counter++, }; for (let shape of parsed[type]) { diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index f3e15550ce4d..8b84562eadae 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -223,25 +223,6 @@ class ShapeCollectionModel extends Listener { return this; } - _createExportContainer() { - const container = {}; - ['create', 'update', 'delete'].forEach( action => { - container[action] = { - "boxes": [], - "box_paths": [], - "points": [], - "points_paths": [], - "polygons": [], - "polygon_paths": [], - "polylines": [], - "polyline_paths": [], - }; - }); - container.pre_erase = this._erased; - - return container; - } - _getExportTargetContainer(export_type, shape_type, container) { let shape_container_target = undefined; let export_action_container = undefined; @@ -401,8 +382,9 @@ class ShapeCollectionModel extends Listener { } export() { - const response = this._createExportContainer(); - this._saved_state = this._createExportContainer(); + const response = createExportContainer(); + this._saved_state = createExportContainer(); + response.pre_erase = this._erased; for (const shape of this._shapes) { if (shape.state === ShapeState.nothing) { diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 0914b54a22e9..83d6b820bd1f 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -496,19 +496,15 @@ class ShapeModel extends Listener { }); this.removed = true; - const newState = this.state === ShapeState.create ? ShapeState.nothing : ShapeState.delete; // Undo/redo code window.cvat.addAction('Remove Object', () => { this.removed = false; - this.state = this._dbId ? ShapeState.update : ShapeState.create; }, () => { this.removed = true; - this.state = newState; }, window.cvat.player.frames.current); // End of undo/redo code - this.state = newState; } set z_order(value) { @@ -526,6 +522,9 @@ class ShapeModel extends Listener { set removed(value) { if (value) { this._active = false; + this.state = this.state === ShapeState.create ? ShapeState.nothing : ShapeState.delete; + } else { + this.state = this._dbId ? ShapeState.update : ShapeState.create; } this._removed = value; this._updateReason = 'remove'; @@ -3205,6 +3204,25 @@ class PointsView extends PolyShapeView { } } +function createExportContainer() { + const container = {}; + ['create', 'update', 'delete'].forEach( action => { + container[action] = { + "boxes": [], + "box_paths": [], + "points": [], + "points_paths": [], + "polygons": [], + "polygon_paths": [], + "polylines": [], + "polyline_paths": [], + }; + }); + container.pre_erase = false; + + return container; +} + function buildShapeModel(data, type, idx, color, initialState) { diff --git a/cvat/apps/tf_annotation/views.py b/cvat/apps/tf_annotation/views.py index 287814f74787..64cfce9697ac 100644 --- a/cvat/apps/tf_annotation/views.py +++ b/cvat/apps/tf_annotation/views.py @@ -106,20 +106,28 @@ def get_image_key(item): def convert_to_cvat_format(data): + def create_anno_container(): + return { + "boxes": [], + "polygons": [], + "polylines": [], + "points": [], + "box_paths": [], + "polygon_paths": [], + "polyline_paths": [], + "points_paths": [], + } + result = { - "boxes": [], - "polygons": [], - "polylines": [], - "points": [], - "box_paths": [], - "polygon_paths": [], - "polyline_paths": [], - "points_paths": [], + 'create': create_anno_container(), + 'update': create_anno_container(), + 'delete': create_anno_container(), + 'pre_erase': True, } for label in data: boxes = data[label] for box in boxes: - result['boxes'].append({ + result['create']['boxes'].append({ "label_id": label, "frame": box[0], "xtl": box[1], @@ -134,7 +142,6 @@ def convert_to_cvat_format(data): return result - def create_thread(id, labels_mapping): try: TRESHOLD = 0.5 From 42108809b9f490df3ca53ce3b832fff6f5dd01aa Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Fri, 5 Oct 2018 11:16:23 +0300 Subject: [PATCH 03/12] using shapes id generated by cleint instead db to update annotations --- cvat/apps/engine/annotation.py | 210 +++++++----------- cvat/apps/engine/models.py | 2 + .../static/engine/js/annotationParser.js | 17 +- .../engine/static/engine/js/annotationUI.js | 6 +- .../static/engine/js/shapeCollection.js | 186 +++++----------- .../engine/static/engine/js/shapeCreator.js | 2 - cvat/apps/engine/static/engine/js/shapes.js | 84 ++----- cvat/apps/engine/views.py | 4 +- cvat/apps/tf_annotation/views.py | 5 +- 9 files changed, 173 insertions(+), 343 deletions(-) diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 822a79adc1f3..033b7a57fadf 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -76,7 +76,6 @@ def save_job(jid, data): Save new annotations for the job. """ db_job = models.Job.objects.select_for_update().get(id=jid) - id_mapping = {} annotation = _AnnotationForJob(db_job) if data['pre_erase']: @@ -84,13 +83,11 @@ def save_job(jid, data): for action in ['create', 'update', 'delete']: annotation.init_from_client(data[action]) - id_mapping[action] = annotation.save_to_db(action) + annotation.save_to_db(action) db_job.segment.task.updated_date = timezone.now() db_job.segment.task.save() - return id_mapping - # pylint: disable=unused-argument def save_task(tid, data): """ @@ -145,14 +142,13 @@ def __init__(self, db_attr, value): self.value = str(value) class _BoundingBox: - def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, db_id=None, client_id=None, attributes=None): + 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.db_id = db_id self.client_id = client_id self.frame = frame self.attributes = attributes if attributes else [] @@ -170,14 +166,14 @@ 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, db_id=None, client_id=None, attributes=None): - super().__init__(x0, y0, x1, y1, frame, occluded, z_order, db_id, client_id, attributes) + 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, None, attributes) + super().__init__(x0, y0, x1, y1, frame, occluded, z_order, None, attributes) self.outside = outside class _InterpolatedBox(_TrackedBox): @@ -186,12 +182,11 @@ def __init__(self, x0, y0, x1, y1, frame, occluded, z_order, outside, keyframe, self.keyframe = keyframe class _PolyShape: - def __init__(self, points, frame, occluded, z_order, db_id=None, client_id=None, attributes=None): + 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.db_id = db_id self.client_id=client_id self.attributes = attributes if attributes else [] @@ -199,14 +194,14 @@ def add_attribute(self, attr): self.attributes.append(attr) class _LabeledPolyShape(_PolyShape): - def __init__(self, label, points, frame, group_id, occluded, z_order, db_id=None,client_id=None, attributes=None): - super().__init__(points, frame, occluded, z_order, db_id, client_id, attributes) + 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, db_id=None, client_id=None, attributes=None): - super().__init__(points, frame, occluded, z_order, db_id, client_id, attributes) + def __init__(self, points, frame, occluded, z_order, outside, client_id=None, attributes=None): + super().__init__(points, frame, occluded, z_order, client_id, attributes) self.outside = outside class _InterpolatedPolyShape(_TrackedPolyShape): @@ -215,13 +210,12 @@ def __init__(self, points, frame, occluded, z_order, outside, keyframe, attribut self.keyframe = keyframe class _BoxPath: - def __init__(self, label, start_frame, stop_frame, group_id, boxes=None, db_id=None, client_id=None, attributes=None): + 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.db_id = db_id self.client_id = client_id self.attributes = attributes if attributes else [] self._interpolated_boxes = [] @@ -291,13 +285,12 @@ def add_attribute(self, attr): self.attributes.append(attr) class _PolyPath: - def __init__(self, label, start_frame, stop_frame, group_id, shapes=None, db_id=None, client_id=None, attributes=None): + 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.db_id = db_id self.client_id = client_id self.attributes = attributes if attributes else [] self._interpolated_shapes = [] # ??? @@ -363,10 +356,10 @@ def to_boxes(self): label=path.label, x0=box.xtl, y0=box.ytl, x1=box.xbr, y1=box.ybr, frame=box.frame, - groupd_id=path.group_id, + group_id=path.group_id, occluded=box.occluded, z_order=box.z_order, - db_id=db_id, + client_id=box.client_id, attributes=box.attributes + path.attributes, ) boxes.append(box) @@ -382,10 +375,10 @@ def _to_poly_shapes(self, iter_attr_name): label=path.label, points=shape.points, frame=shape.frame, - groupd_id=path.group_id, + group_id=path.group_id, occluded=shape.occluded, z_order=shape.z_order, - db_id=shape.db_id, + client_id=shape.client_id, attributes=shape.attributes + path.attributes, ) shapes.append(shape) @@ -414,8 +407,8 @@ def to_box_paths(self): stop_frame=box.frame + 1, group_id=box.group_id, boxes=[box0, box1], - db_id=box.db_id, attributes=box.attributes, + client_id=box.client_id, ) paths.append(path) @@ -435,7 +428,7 @@ def _to_poly_paths(self, iter_attr_name): stop_frame=shape.frame + 1, group_id=shape.group_id, shapes=[shape0, shape1], - db_id=shape.bd_id, + client_id=shape.client_id, attributes=shape.attributes, ) paths.append(path) @@ -530,7 +523,7 @@ def _clamp_poly(self, points, im_size): def _get_values(shape_type): if shape_type == 'polygons': return [ - ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', + ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id', 'labeledpolygonattributeval__value', 'labeledpolygonattributeval__spec_id', 'labeledpolygonattributeval__id'), { 'attributes': [ @@ -542,7 +535,7 @@ def _get_values(shape_type): ] elif shape_type == 'polylines': return [ - ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', + ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id', 'labeledpolylineattributeval__value', 'labeledpolylineattributeval__spec_id', 'labeledpolylineattributeval__id'), { 'attributes': [ @@ -554,7 +547,7 @@ def _get_values(shape_type): ] elif shape_type == 'boxes': return [ - ('id', 'frame', 'xtl', 'ytl', 'xbr', 'ybr', 'label_id', 'group_id', 'occluded', 'z_order', + ('id', 'frame', 'xtl', 'ytl', 'xbr', 'ybr', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id', 'labeledboxattributeval__value', 'labeledboxattributeval__spec_id', 'labeledboxattributeval__id'), { 'attributes': [ @@ -566,7 +559,7 @@ def _get_values(shape_type): ] elif shape_type == 'points': return [ - ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', + ('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id', 'labeledpointsattributeval__value', 'labeledpointsattributeval__spec_id', 'labeledpointsattributeval__id'), { 'attributes': [ @@ -594,7 +587,7 @@ def init_from_db(self): group_id=db_shape.group_id, occluded=db_shape.occluded, z_order=db_shape.z_order, - db_id=db_shape.id, + client_id=db_shape.client_id, ) else: shape = _LabeledPolyShape( @@ -604,7 +597,7 @@ def init_from_db(self): group_id=db_shape.group_id, occluded=db_shape.occluded, z_order=db_shape.z_order, - db_id=db_shape.id, + client_id=db_shape.client_id, ) for db_attr in db_shape.attributes: if db_attr.id != None: @@ -622,7 +615,7 @@ def init_from_db(self): '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', 'objectpathattributeval__spec_id', + 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', @@ -745,7 +738,7 @@ def init_from_db(self): start_frame=db_path.frame, stop_frame=self.stop_frame, group_id=db_path.group_id, - db_id=db_path.id, + client_id=db_path.client_id, ) for db_attr in db_path.attributes: spec = self.db_attributes[db_attr.spec_id] @@ -793,6 +786,7 @@ def init_from_db(self): occluded=db_shape.occluded, z_order=db_shape.z_order, outside=db_shape.outside, + client_id=db_shape.client_id, ) assert shape.frame > frame frame = shape.frame @@ -829,7 +823,6 @@ def init_from_client(self, data): group_id=int(box['group_id']), occluded=strtobool(str(box['occluded'])), z_order=int(box['z_order']), - db_id=int(box['db_id']) if 'db_id' in box and box['db_id'] else None, client_id=int(box['client_id']), ) @@ -853,7 +846,6 @@ def init_from_client(self, data): group_id=int(poly_shape['group_id']), occluded=poly_shape['occluded'], z_order=int(poly_shape['z_order']), - db_id=int(poly_shape['db_id']) if 'db_id' in poly_shape and poly_shape['db_id'] else None, client_id=int(poly_shape['client_id']), ) @@ -922,7 +914,6 @@ def init_from_client(self, data): stop_frame=self.stop_frame, group_id=int(path['group_id']), boxes=boxes, - db_id=int(path['db_id']) if 'db_id' in path and path['db_id'] else None, client_id=int(path['client_id']), attributes=attributes, ) @@ -986,7 +977,6 @@ def init_from_client(self, data): stop_frame=self.stop_frame + 1, group_id=int(path['group_id']), shapes=poly_shapes, - db_id=int(path['db_id']) if 'db_id' in path and path['db_id'] else None, client_id=int(path['client_id']), attributes=attributes, ) @@ -1030,8 +1020,6 @@ def _get_shape_attr_class(self, shape_type): return models.TrackedPointsAttributeVal def _save_paths_to_db(self): - id_mapping = {} - for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: db_paths = [] db_path_attrvals = [] @@ -1045,6 +1033,7 @@ def _save_paths_to_db(self): 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': @@ -1113,8 +1102,6 @@ def _save_paths_to_db(self): elif shape_type == 'points_paths': db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="points")) - id_mapping[shape_type] = {client_shape.client_id: db_shape.id for client_shape, db_shape in zip(shapes, db_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) @@ -1144,8 +1131,6 @@ def _save_paths_to_db(self): self._get_shape_attr_class(shape_type).objects.bulk_create(db_shape_attrvals) - return id_mapping - def _get_shape_set(self, shape_type): if shape_type == 'polygons': return self.db_job.labeledpolygon_set @@ -1160,8 +1145,6 @@ def _save_shapes_to_db(self): db_shapes = [] db_attrvals = [] - id_mapping = {} - for shape_type in ['polygons', 'polylines', 'points', 'boxes']: db_shapes = [] db_attrvals = [] @@ -1172,6 +1155,7 @@ def _save_shapes_to_db(self): 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 @@ -1210,7 +1194,6 @@ def _save_shapes_to_db(self): # are auto incremented. Thus we will not be inside the 'if'. db_shapes = list(self._get_shape_set(shape_type).exclude(id__in=db_shapes_ids)) - id_mapping[shape_type] = {client_shape.client_id: db_shape.id for client_shape, db_shape in zip(shapes, db_shapes) } for db_attrval in db_attrvals: if shape_type == 'polygons': @@ -1223,7 +1206,6 @@ def _save_shapes_to_db(self): db_attrval.points_id = db_shapes[db_attrval.points_id].id self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) - return id_mapping def _get_relaited_obj_field_name(self, shape_type): 'polygon_paths', 'polyline_paths', 'points_paths', 'box_paths' @@ -1237,46 +1219,43 @@ def _get_relaited_obj_field_name(self, shape_type): return 'polyline' def _update_shapes_to_db(self): - updated_ids = {} for shape_type in ['boxes', 'points', 'polygons', 'polylines']: db_attrvals = [] - updated_ids[shape_type] = [] - shapes_to_update = {shape.db_id: shape for shape in getattr(self, shape_type)} + shapes_to_update = {shape.client_id: shape for shape in getattr(self, shape_type)} if not shapes_to_update: continue - db_ids_to_update = list(shapes_to_update.keys()) + client_ids_to_update = list(shapes_to_update.keys()) - attr_filter = {'{}__id__in'.format(self._get_relaited_obj_field_name(shape_type)):db_ids_to_update} + attr_filter = {'{}__client_id__in'.format(self._get_relaited_obj_field_name(shape_type)):client_ids_to_update} self._get_shape_attr_class(shape_type).objects.filter(**attr_filter).delete() for shape_id_to_update, shape_to_update in shapes_to_update.items(): + shape = self._get_shape_class(shape_type).objects.get(job_id=self.db_job.id, client_id=shape_id_to_update) + shape.label = self.db_labels[shape_to_update.label.id] + shape.group_id = shape_to_update.group_id + shape.frame = shape_to_update.frame + shape.occluded = shape_to_update.occluded + shape.z_order = shape_to_update.z_order if shape_type == 'boxes': - self._get_shape_class(shape_type).objects.filter(id=shape_id_to_update).update( - label = shape_to_update.label.id, - group_id = shape_to_update.group_id, - xtl = shape_to_update.xtl, - ytl = shape_to_update.ytl, - xbr = shape_to_update.xbr, - ybr = shape_to_update.ybr, - ) + shape.xtl = shape_to_update.xtl + shape.ytl = shape_to_update.ytl + shape.xbr = shape_to_update.xbr + shape.ybr = shape_to_update.ybr else: - self._get_shape_class(shape_type).objects.filter(id=shape_id_to_update).update( - label = shape_to_update.label.id, - group_id = shape_to_update.group_id, - points = shape_to_update.points - ) + shape.points = shape_to_update.points + shape.save() for attr in shape_to_update.attributes: db_attrval = self._get_shape_attr_class(shape_type)() if shape_type == 'polygons': - db_attrval.polygon_id = shape_id_to_update + db_attrval.polygon_id = shape.id elif shape_type == 'polylines': - db_attrval.polyline_id = shape_id_to_update + db_attrval.polyline_id = shape.id elif shape_type == 'boxes': - db_attrval.box_id = shape_id_to_update + db_attrval.box_id = shape.id else: - db_attrval.points_id = shape_id_to_update + db_attrval.points_id = shape.id db_attrval.spec = self.db_attributes[attr.id] db_attrval.value = attr.value @@ -1284,47 +1263,38 @@ def _update_shapes_to_db(self): self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) - updated_client_ids = list(shape.client_id for shape in shapes_to_update.values()) - updated_ids[shape_type] = updated_client_ids - - return updated_ids - def _update_paths_to_db(self): - updated_ids = {} for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: db_path_attrvals = [] db_shapes = [] db_shape_attrvals = [] - updated_ids[shape_type] = [] - shapes_to_update = {shape.db_id: shape for shape in getattr(self, shape_type)} + shapes_to_update = {shape.client_id: shape for shape in getattr(self, shape_type)} if not shapes_to_update: continue - db_ids_to_update = list(shapes_to_update.keys()) + client_ids_to_update = list(shapes_to_update.keys()) - if None in db_ids_to_update: + if None in client_ids_to_update: raise Exception('Trying to update None id') - #delete shape attributes - models.ObjectPathAttributeVal.objects.filter(track__id__in=db_ids_to_update).delete() + models.ObjectPathAttributeVal.objects.filter(track__job__id=self.db_job.id, track__client_id__in=client_ids_to_update).delete() - # delete all relaited paths with path_attributes shape_class = self._get_shape_class(shape_type) - shape_class.objects.filter(track__id__in=db_ids_to_update).delete() + shape_class.objects.filter(track__job__id=self.db_job.id, track__client_id__in=client_ids_to_update).delete() # update shape props for shape_id_to_update, shape_to_update in shapes_to_update.items(): - models.ObjectPath.objects.filter(id=shape_id_to_update).update( - label = shape_to_update.label.id, - frame = shape_to_update.frame, - group_id = shape_to_update.group_id, - ) + shape = models.ObjectPath.objects.get(job_id=self.db_job.id, client_id=shape_id_to_update) + shape.label = self.db_labels[shape_to_update.label.id] + shape.frame = shape_to_update.frame + shape.group_id = shape_to_update.group_id + shape.save() for attr in shape_to_update.attributes: db_attrspec = self.db_attributes[attr.id] db_attrval = models.ObjectPathAttributeVal() - db_attrval.track_id = shape_id_to_update + db_attrval.track_id = shape.id db_attrval.spec = db_attrspec db_attrval.value = attr.value db_path_attrvals.append(db_attrval) @@ -1332,7 +1302,7 @@ def _update_paths_to_db(self): path_shapes = shape_to_update.boxes if hasattr(shape_to_update, 'boxes') else shape_to_update.shapes for shape in path_shapes: db_shape = shape_class() - db_shape.track_id = shape_id_to_update + db_shape.track_id = shape.id if shape_type == 'box_paths': db_shape.xtl = shape.xtl db_shape.ytl = shape.ytl @@ -1373,7 +1343,7 @@ def _update_paths_to_db(self): # 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)) + db_shapes = list(self._get_shape_class(shape_type).objects.filter(track__job_id=self.db_job.id).exclude(id__in=db_shapes_ids)) for db_attrval in db_shape_attrvals: if shape_type == 'polygon_paths': @@ -1387,41 +1357,23 @@ def _update_paths_to_db(self): self._get_shape_attr_class(shape_type).objects.bulk_create(db_shape_attrvals) - updated_client_ids = list(shape.client_id for shape in shapes_to_update.values()) - updated_ids[shape_type] = updated_client_ids - - return updated_ids - def _delete_shapes_from_db(self): - deleted_ids = {} for shape_type in ['polygons', 'polylines', 'points', 'boxes']: - db_ids_to_delete = list(shape.db_id for shape in getattr(self, shape_type)) - deleted = self._get_shape_set(shape_type).filter(id__in=db_ids_to_delete).delete() + db_ids_to_delete = list(shape.client_id for shape in getattr(self, shape_type)) + deleted = self._get_shape_set(shape_type).filter(client_id__in=db_ids_to_delete).delete() class_name = 'engine.{}'.format(self._get_shape_class(shape_type).__name__) if not (deleted[0] == 0 and len(db_ids_to_delete) == 0) and (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): raise Exception('Number of deleted object doesn\'t match with requested number') - deleted_client_ids = list(shape.client_id for shape in getattr(self, shape_type)) - deleted_ids[shape_type] = deleted_client_ids - - return deleted_ids - def _delete_paths_from_db(self): - deleted_ids = {} for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: - db_ids_to_delete = list(shape.db_id for shape in getattr(self, shape_type)) - deleted = self.db_job.objectpath_set.filter(id__in=db_ids_to_delete).delete() + db_ids_to_delete = list(shape.client_id for shape in getattr(self, shape_type)) + deleted = self.db_job.objectpath_set.filter(client_id__in=db_ids_to_delete).delete() class_name = 'engine.ObjectPath' if not (deleted[0] == 0 and len(db_ids_to_delete) == 0) and \ (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): raise Exception('Number of deleted object doesn\'t match with requested number') - - deleted_client_ids = list(shape.client_id for shape in getattr(self, shape_type)) - deleted_ids[shape_type] = deleted_client_ids - - return deleted_ids - def _delete_all_shapes_from_db(self): for shape_type in ['polygons', 'polylines', 'points', 'boxes']: self._get_shape_set(shape_type).all().delete() @@ -1431,16 +1383,14 @@ def _delete_all_paths_from_db(self): def save_to_db(self, action): if action == 'create': - shape_mapping = self._save_shapes_to_db() - path_mapping = self._save_paths_to_db() + self._save_shapes_to_db() + self._save_paths_to_db() elif action == 'update': - shape_mapping = self._update_shapes_to_db() - path_mapping = self._update_paths_to_db() + self._update_shapes_to_db() + self._update_paths_to_db() elif action == 'delete': - shape_mapping = self._delete_shapes_from_db() - path_mapping = self._delete_paths_from_db() - - return {**shape_mapping, **path_mapping} + self._delete_shapes_from_db() + self._delete_paths_from_db() def delete_objs_from_db(self): self._delete_all_shapes_from_db() @@ -1460,7 +1410,7 @@ def to_client(self): for box in self.boxes: data["boxes"].append({ - "db_id": box.db_id, + "client_id": box.client_id, "label_id": box.label.id, "group_id": box.group_id, "xtl": box.xtl, @@ -1476,7 +1426,7 @@ def to_client(self): for poly_type in ['polygons', 'polylines', 'points']: for poly in getattr(self, poly_type): data[poly_type].append({ - "db_id": poly.db_id, + "client_id": poly.client_id, "label_id": poly.label.id, "group_id": poly.group_id, "points": poly.points, @@ -1488,14 +1438,13 @@ def to_client(self): for box_path in self.box_paths: data["box_paths"].append({ - "db_id": box_path.db_id, + "client_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: ({ - "db_id": box.db_id, "frame": box.frame, "xtl": box.xtl, "ytl": box.ytl, @@ -1512,14 +1461,13 @@ def to_client(self): 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({ - "db_id": poly_path.db_id, + "client_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: ({ - "db_id": shape.db_id, "frame": shape.frame, "points": shape.points, "occluded": shape.occluded, @@ -1651,7 +1599,7 @@ def close_root(self): class _XmlAnnotationWriter(_AnnotationWriter): def __init__(self, file): - super().__init__(file, "1.0") + super().__init__(file, "1.1") self.xmlgen = XMLGenerator(self.file, 'utf-8') self._level = 0 @@ -2077,7 +2025,7 @@ def _flip_shape(shape, im_w, im_h): ("xbr", "{:.2f}".format(shape.xbr)), ("ybr", "{:.2f}".format(shape.ybr)), ("occluded", str(int(shape.occluded))), - ("db_id", "{}".format(shape.db_id)), + ("client_id", "{}".format(shape.client_id)), ]) if db_task.z_order: dump_dict['z_order'] = str(shape.z_order) @@ -2097,7 +2045,7 @@ def _flip_shape(shape, im_w, im_h): )) for p in shape.points.split(' ')) )), ("occluded", str(int(shape.occluded))), - ("db_id", "{}".format(shape.db_id)), + ("client_id", "{}".format(shape.client_id)), ]) if db_task.z_order: @@ -2145,7 +2093,7 @@ def _flip_shape(shape, im_w, im_h): dump_dict = OrderedDict([ ("id", str(path_idx)), ("label", path.label.name), - ("db_id", "{}".format(path.db_id)), + ("client_id", "{}".format(path.client_id)), ]) if path.group_id: dump_dict['group_id'] = str(path.group_id) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 6883018b194b..8b21974c2162 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -145,6 +145,7 @@ class Meta: class Shape(models.Model): occluded = models.BooleanField(default=False) z_order = models.IntegerField(default=0) + client_id = models.IntegerField(default=-1) class Meta: abstract = True @@ -189,6 +190,7 @@ class LabeledPointsAttributeVal(AttributeVal): class ObjectPath(Annotation): id = models.BigAutoField(primary_key=True) + client_id = models.IntegerField(default=-1) shapes = models.CharField(max_length=10, default='boxes') class ObjectPathAttributeVal(AttributeVal): diff --git a/cvat/apps/engine/static/engine/js/annotationParser.js b/cvat/apps/engine/static/engine/js/annotationParser.js index c4054745b7e7..f90967d8e9b9 100644 --- a/cvat/apps/engine/static/engine/js/annotationParser.js +++ b/cvat/apps/engine/static/engine/js/annotationParser.js @@ -52,7 +52,8 @@ class AnnotationParser { let occluded = +box.getAttribute('occluded'); let z_order = box.getAttribute('z_order') || '0'; - return [xtl, ytl, xbr, ybr, occluded, +z_order]; + let client_id = box.getAttribute('client_id') || '-1' + return [xtl, ytl, xbr, ybr, occluded, +z_order, +client_id]; } _getPolyPosition(shape, frame) { @@ -78,7 +79,8 @@ class AnnotationParser { let occluded = +shape.getAttribute('occluded'); let z_order = shape.getAttribute('z_order') || '0'; - return [points, occluded, +z_order]; + let client_id = box.getAttribute('client_id') || '-1' + return [points, occluded, +z_order, +client_id]; } _getAttribute(labelId, attrTag) { @@ -213,7 +215,7 @@ class AnnotationParser { let attributeList = this._getAttributeList(shape, labelId); if (shape_type === 'boxes') { - let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, frame); + let [xtl, ytl, xbr, ybr, occluded, z_order, client_id] = this._getBoxPosition(shape, frame); data.boxes.push({ label_id: labelId, group_id: +groupId, @@ -225,7 +227,7 @@ class AnnotationParser { ybr: ybr, z_order: z_order, attributes: attributeList, - client_id: this.counter++, + client_id: client_id !== -1 ? client_id : this.counter++, }); } else { @@ -238,7 +240,7 @@ class AnnotationParser { occluded: occluded, z_order: z_order, attributes: attributeList, - client_id: this.counter++, + client_id: client_id !== -1 ? client_id : this.counter++, }); } } @@ -258,7 +260,8 @@ class AnnotationParser { let tracks = xml.getElementsByTagName('track'); for (let track of tracks) { let labelId = this._labelsInfo.labelIdOf(track.getAttribute('label')); - let groupId = track.getAttribute('group_id') || "0"; + let groupId = track.getAttribute('group_id') || '0'; + let client_id = track.getAttribute('client_id') || '-1'; if (labelId === null) { throw Error('An unknown label found in the annotation file: ' + name); } @@ -311,7 +314,7 @@ class AnnotationParser { frame: +parsed[type][0].getAttribute('frame'), attributes: [], shapes: [], - client_id: this.counter++, + client_id: client_id !== -1 ? client_id : this.counter++, }; for (let shape of parsed[type]) { diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 9ac43ab36f62..e603468a795e 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -614,16 +614,15 @@ function saveAnnotation(shapeCollectionModel, job) { const annotationLogs = Logger.getLogs(); const data = { - annotation: exportedData, + annotation: JSON.stringify(exportedData), logs: JSON.stringify(annotationLogs.export()), }; saveButton.prop('disabled', true); saveButton.text('Saving..'); - saveJobRequest(job.jobid, data, (response) => { + saveJobRequest(job.jobid, data, () => { // success - shapeCollectionModel.syncWithDB(response, true); shapeCollectionModel.updateHash(); saveButton.text('Success!'); setTimeout(() => { @@ -632,7 +631,6 @@ function saveAnnotation(shapeCollectionModel, job) { }, 3000); }, (response) => { // error - shapeCollectionModel.syncWithDB(response, false); saveButton.prop('disabled', false); saveButton.text('Save Work'); let message = `Impossible to save job. Errors was occured. Status: ${response.status}`; diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 8b84562eadae..b0bb97e760a1 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -51,8 +51,8 @@ class ShapeCollectionModel extends Listener { this._colorIdx = 0; this._filter = new FilterModel(() => this.update()); this._splitter = new ShapeSplitter(); - this._saved_state = {}; this._erased = false; + this._initialShapes = []; } _nextIdx() { @@ -186,37 +186,37 @@ class ShapeCollectionModel extends Listener { } } - import(data, initialState = ShapeState.nothing) { + import(data) { for (let box of data.boxes) { - this.add(box, 'annotation_box', initialState); + this.add(box, 'annotation_box'); } for (let box_path of data.box_paths) { - this.add(box_path, 'interpolation_box', initialState); + this.add(box_path, 'interpolation_box'); } for (let points of data.points) { - this.add(points, 'annotation_points', initialState); + this.add(points, 'annotation_points'); } for (let points_path of data.points_paths) { - this.add(points_path, 'interpolation_points', initialState); + this.add(points_path, 'interpolation_points'); } for (let polygon of data.polygons) { - this.add(polygon, 'annotation_polygon', initialState); + this.add(polygon, 'annotation_polygon'); } for (let polygon_path of data.polygon_paths) { - this.add(polygon_path, 'interpolation_polygon', initialState); + this.add(polygon_path, 'interpolation_polygon'); } for (let polyline of data.polylines) { - this.add(polyline, 'annotation_polyline', initialState); + this.add(polyline, 'annotation_polyline'); } for (let polyline_path of data.polyline_paths) { - this.add(polyline_path, 'interpolation_polyline', initialState); + this.add(polyline_path, 'interpolation_polyline'); } this.notify(); @@ -271,134 +271,36 @@ class ShapeCollectionModel extends Listener { return shape_container_target; } - _updateShapeDbIds(response) { - /* - Function updates db ids for successfully saved objects and makes simple sanity checks. - */ - for (const shape_type in response['create']) { - const shapes = response['create'][shape_type]; - const saved_states = this._saved_state['create'][shape_type]; - for (const shape_client_id in shapes) { - //check saved state - if (!saved_states.includes(+shape_client_id)) { - throw Error('Unexpected behaviour: cretaed object has unexpected client id'); - } - // current state cannot be created - if (this._shapes[shape_client_id] === ShapeState.create) { - throw Error('Unexpected behaviour: unexpected current state for created object'); - } - this._shapes[shape_client_id]._dbId = shapes[shape_client_id]; - } - } - - for (const shape_type in response['update']) { - const shapes = response['update'][shape_type]; - const saved_states = this._saved_state['update'][shape_type]; - for (const shape_client_id of shapes) { - //check saved state - if (!saved_states.includes(+shape_client_id)) { - throw Error('Unexpected behaviour: updated object has unexpected client id'); - } - // current state cannot be created - if (this._shapes[shape_client_id] === ShapeState.create) { - throw Error('Unexpected behaviour: unexpected current state for updated object'); - } - } - } - - for (const shape_type in response['delete']) { - const shapes = response['delete'][shape_type]; - const saved_states = this._saved_state['delete'][shape_type]; - for (const shape_client_id of shapes) { - //check saved state - if (!saved_states.includes(+shape_client_id)) { - throw Error('Unexpected behaviour: deleted object has unexpected client id'); - } - - if (this._shapes[shape_client_id] === ShapeState.created) { - throw Error('Unexpected behaviour: unexpected current state for deleted object'); - } - // TODO: check need of this condition - if (this._shapes[shape_client_id] === ShapeState.delete) { - this._shapes[shape_client_id].state = ShapeState.nothing; - } - // object was deleted and has no db id - this._shapes[shape_client_id]._dbId = null; - } - } - } - - _revertUnsavedStates() { - /* - Function reverts shape export states in case of failed save request - */ - for (const shape_type in this._saved_state['create']) { - for (const shape_client_id of this._saved_state['create'][shape_type]) { - const currentState = this._shapes[shape_client_id].state; - if (currentState === ShapeState.delete) { - this._shapes[shape_client_id].state = ShapeState.nothing; - } else { - this._shapes[shape_client_id].state = ShapeState.create; - } - } - } - - for (const shape_type in this._saved_state['update']) { - for (const shape_client_id of this._saved_state['update'][shape_type]) { - const currentState = this._shapes[shape_client_id].state; - if (currentState === ShapeState.create) { - throw Error('Unexpected behaviour: unexpected current state for object that should be updated'); - } else if (currentState === ShapeState.nothing) { - this._shapes[shape_client_id].state = ShapeState.update; - } - } - } - - for (const shape_type in this._saved_state['delete']) { - for (const shape_client_id of this._saved_state['delete'][shape_type]) { - const currentState = this._shapes[shape_client_id].state; - if (currentState === ShapeState.create) { - throw Error('Unexpected behaviour: unexpected current state for object that should be deleted'); - } else if (currentState === ShapeState.nothing) { - this._shapes[shape_client_id].state = ShapeState.delete; - } - } - } - } - - syncWithDB(response, isSuccess) { - if (isSuccess) { - this._updateShapeDbIds(response); - } else { - this._revertUnsavedStates(); - } - } - reset_state() { - for (const shape of this._shapes) { - shape.state = ShapeState.nothing; - } this._erased = false; } export() { const response = createExportContainer(); - this._saved_state = createExportContainer(); response.pre_erase = this._erased; for (const shape of this._shapes) { - if (shape.state === ShapeState.nothing) { + let target_export_container = undefined; + if (!shape._removed) { + if (!(shape.id in this._initialShapes)) { + target_export_container = this._getExportTargetContainer(ShapeState.create, shape.type, response); + } else if (JSON.stringify(this._initialShapes[shape.id]) !== JSON.stringify(shape.export())) { + target_export_container = this._getExportTargetContainer(ShapeState.update, shape.type, response); + } else { + continue; + } + } + else if (shape.id in this._initialShapes) { + // TODO in this case need push only id + target_export_container = this._getExportTargetContainer(ShapeState.delete, shape.type, response); + } + else { continue; } - const target_export_container = this._getExportTargetContainer(shape.state, shape.type, response); target_export_container.push(shape.export()); - - const saved_state_container_target = this._getExportTargetContainer(shape.state, shape.type, this._saved_state); - saved_state_container_target.push(shape.id); } - - return JSON.stringify(response); + return response; } find(direction) { @@ -457,11 +359,25 @@ class ShapeCollectionModel extends Listener { } hasUnsavedChanges() { - return md5(this.export()) !== this._hash; + const exportData = this.export(); + for (const action of ['create', 'update', 'delete']) { + for (const shapes of Object.values(exportData[action])) { + if (shapes.length) { + return true; + } + } + } + + return exportData.pre_erase; } updateHash() { - this._hash = md5(this.export()); + this._initialShapes = {}; + for (const shape of this._shapes) { + if (!shape.removed) { + this._initialShapes[shape.id] = shape.export(); + } + } return this; } @@ -475,8 +391,22 @@ class ShapeCollectionModel extends Listener { this._interpolate(); } - add(data, type, initialState=ShapeState.create) { - let model = buildShapeModel(data, type, this._nextIdx(), this.nextColor(), initialState); + add(data, type) { + let id = null; + + if (!('client_id' in data) || data.client_id < 0) { + id = this._nextIdx(); + } + else if (data.client_id === -1 ) { + this._erased = true; + id = this._nextIdx(); + } + else { + id = data.client_id; + this._idx = Math.max(this._idx, id) + 1; + } + + let model = buildShapeModel(data, type, id, this.nextColor()); if (type.startsWith('interpolation')) { this._interpolationShapes.push(model); } diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js index a24933ba07be..eebcb878c217 100644 --- a/cvat/apps/engine/static/engine/js/shapeCreator.js +++ b/cvat/apps/engine/static/engine/js/shapeCreator.js @@ -56,12 +56,10 @@ class ShapeCreatorModel extends Listener { // Undo/redo code window.cvat.addAction('Draw Object', () => { model.removed = true; - model.state = model._dbId ? ShapeState.delete : ShapeState.nothing; model.unsubscribe(this._shapeCollection); }, () => { model.subscribe(this._shapeCollection); model.removed = false; - model.state = ShapeState.create; }, window.cvat.player.frames.current); // End of undo/redo code diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 83d6b820bd1f..3bb8fd105407 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -23,7 +23,7 @@ const ShapeState = Object.freeze({ }); class ShapeModel extends Listener { - constructor(data, positions, type, id, color, exportState) { + constructor(data, positions, type, id, color) { super('onShapeUpdate', () => this ); this._id = id; this._groupId = data.group_id; @@ -45,8 +45,6 @@ class ShapeModel extends Listener { this._hiddenText = true; this._updateReason = null; this._importAttributes(data.attributes, positions); - this._dbId = data.db_id ? data.db_id : null; - this._exportState = exportState; } _importAttributes(attributes, positions) { @@ -239,9 +237,6 @@ class ShapeModel extends Listener { window.cvat.addAction('Change Attribute', () => { if (typeof(oldAttr) === 'undefined') { delete this._attributes.mutable[frame][attrId]; - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } this._updateReason = 'attributes'; this.notify(); } @@ -262,10 +257,6 @@ class ShapeModel extends Listener { this._attributes.immutable[attrId] = labelsInfo.strToValues(attrInfo.type, value)[0]; } - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } - this._updateReason = 'attributes'; this.notify(); } @@ -280,9 +271,6 @@ class ShapeModel extends Listener { this._label = +labelId; this._importAttributes([], []); this._setupKeyFrames(); - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } this._updateReason = 'changelabel'; this.notify(); } @@ -317,9 +305,6 @@ class ShapeModel extends Listener { // End of undo/redo code this.updatePosition(frame, position, true); - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } this._updateReason = 'occluded'; this.notify(); } @@ -370,9 +355,6 @@ class ShapeModel extends Listener { } this._updateReason = 'outside'; - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } this.notify(); } else { @@ -386,9 +368,6 @@ class ShapeModel extends Listener { let position = this._interpolatePosition(frame); position.outside = !position.outside; this.updatePosition(frame, position, true); - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } // Update the start frame if need and redestribute attributes if (frame < this._frame) { @@ -440,9 +419,6 @@ class ShapeModel extends Listener { this._frame = frame; } } - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } this._updateReason = 'keyframe'; this.notify(); } @@ -514,7 +490,6 @@ class ShapeModel extends Listener { position.z_order = value; this.updatePosition(frame, position, true); this._updateReason = 'z_order'; - this.state = ShapeState.update; this.notify(); } } @@ -522,23 +497,13 @@ class ShapeModel extends Listener { set removed(value) { if (value) { this._active = false; - this.state = this.state === ShapeState.create ? ShapeState.nothing : ShapeState.delete; - } else { - this.state = this._dbId ? ShapeState.update : ShapeState.create; } + this._removed = value; this._updateReason = 'remove'; this.notify(); } - set state(state) { - this._exportState = state; - } - - get state() { - return this._exportState; - } - get removed() { return this._removed; } @@ -644,8 +609,8 @@ class ShapeModel extends Listener { class BoxModel extends ShapeModel { - constructor(data, type, id, color, state) { - super(data, data.shapes || [], type, id, color, state); + constructor(data, type, id, color) { + super(data, data.shapes || [], type, id, color); this._positions = BoxModel.importPositions.call(this, data.shapes || data); this._setupKeyFrames(); } @@ -728,9 +693,6 @@ class BoxModel extends ShapeModel { window.cvat.addAction('Change Position', () => { if (!Object.keys(oldPos).length) { delete this._positions[frame]; - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } this._updateReason = 'position'; this.notify(); } @@ -751,10 +713,6 @@ class BoxModel extends ShapeModel { outside: position.outside, }); } - - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } } if (!silent) { @@ -815,7 +773,6 @@ class BoxModel extends ShapeModel { } return Object.assign({}, this._positions[this._frame], { - db_id: this._dbId, client_id: this._id, attributes: immutableAttributes, label_id: this._label, @@ -825,7 +782,6 @@ class BoxModel extends ShapeModel { } else { let boxPath = { - db_id: this._dbId, client_id: this._id, label_id: this._label, group_id: this._groupId, @@ -918,13 +874,12 @@ class BoxModel extends ShapeModel { } class PolyShapeModel extends ShapeModel { - constructor(data, type, id, color, state) { - super(data, data.shapes || [], type, id, color, state); + constructor(data, type, id, color) { + super(data, data.shapes || [], type, id, color); this._positions = PolyShapeModel.importPositions.call(this, data.shapes || data); this._setupKeyFrames(); } - _interpolatePosition(frame) { if (frame in this._positions) { return Object.assign({}, this._positions[frame], { @@ -986,9 +941,6 @@ class PolyShapeModel extends ShapeModel { outside: position.outside, }); } - if (this.state !== ShapeState.create) { - this.state = ShapeState.update; - } } if (!silent) { @@ -1017,7 +969,6 @@ class PolyShapeModel extends ShapeModel { } return Object.assign({}, this._positions[this._frame], { - db_id: this._dbId, client_id: this._id, attributes: immutableAttributes, label_id: this._label, @@ -1027,7 +978,6 @@ class PolyShapeModel extends ShapeModel { } else { let polyPath = { - db_id: this._dbId, client_id: this._id, label_id: this._label, group_id: this._groupId, @@ -1133,8 +1083,8 @@ class PolyShapeModel extends ShapeModel { } class PointsModel extends PolyShapeModel { - constructor(data, type, id, color, state) { - super(data, type, id, color, state); + constructor(data, type, id, color) { + super(data, type, id, color); this._minPoints = 1; } @@ -1159,8 +1109,8 @@ class PointsModel extends PolyShapeModel { class PolylineModel extends PolyShapeModel { - constructor(data, type, id, color, state) { - super(data, type, id, color, state); + constructor(data, type, id, color) { + super(data, type, id, color); this._minPoints = 2; } @@ -1195,8 +1145,8 @@ class PolylineModel extends PolyShapeModel { class PolygonModel extends PolyShapeModel { - constructor(data, type, id, color, state) { - super(data, type, id, color, state); + constructor(data, type, id, color) { + super(data, type, id, color); this._minPoints = 3; this._draggable = false; } @@ -3225,20 +3175,20 @@ function createExportContainer() { -function buildShapeModel(data, type, idx, color, initialState) { +function buildShapeModel(data, type, idx, color) { switch (type) { case 'interpolation_box': case 'annotation_box': - return new BoxModel(data, type, idx, color, initialState); + return new BoxModel(data, type, idx, color); case 'interpolation_points': case 'annotation_points': - return new PointsModel(data, type, idx, color, initialState); + return new PointsModel(data, type, idx, color); case 'interpolation_polyline': case 'annotation_polyline': - return new PolylineModel(data, type, idx, color, initialState); + return new PolylineModel(data, type, idx, color); case 'interpolation_polygon': case 'annotation_polygon': - return new PolygonModel(data, type, idx, color, initialState); + return new PolygonModel(data, type, idx, color); } throw Error('Unreacheable code was reached.'); } diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 3c2d425ec77b..e445a6861aa1 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -241,7 +241,7 @@ def save_annotation_for_job(request, jid): job_logger[jid].info("save annotation for {} job".format(jid)) data = json.loads(request.body.decode('utf-8')) if 'annotation' in data: - mapping = annotation.save_job(jid, json.loads(data['annotation'])) + annotation.save_job(jid, json.loads(data['annotation'])) if 'logs' in data: client_log_proxy.push_logs(jid, json.loads(data['logs'])) except RequestException as e: @@ -251,7 +251,7 @@ def save_annotation_for_job(request, jid): job_logger[jid].error("cannot save annotation for job {}".format(jid), exc_info=True) return HttpResponseBadRequest(str(e)) - return JsonResponse(mapping) + return HttpResponse() @login_required @permission_required(perm=['engine.view_task', 'engine.change_annotation'], raise_exception=True) diff --git a/cvat/apps/tf_annotation/views.py b/cvat/apps/tf_annotation/views.py index 64cfce9697ac..725cd5d279ea 100644 --- a/cvat/apps/tf_annotation/views.py +++ b/cvat/apps/tf_annotation/views.py @@ -126,7 +126,7 @@ def create_anno_container(): } for label in data: boxes = data[label] - for box in boxes: + for i, box in enumerate(boxes): result['create']['boxes'].append({ "label_id": label, "frame": box[0], @@ -137,7 +137,8 @@ def create_anno_container(): "z_order": 0, "group_id": 0, "occluded": False, - "attributes": [] + "attributes": [], + "client_id": i, }) return result From 26f6cdaa6a67af0ea63f211aefc38af00cef463d Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Fri, 5 Oct 2018 11:39:41 +0300 Subject: [PATCH 04/12] removed md5.js, code cleanup --- cvat/apps/engine/annotation.py | 108 ++++--- .../migrations/0010_auto_20181002_1122.py | 58 ++++ .../engine/static/engine/js/3rdparty/md5.js | 280 ------------------ .../engine/static/engine/js/annotationUI.js | 2 +- .../engine/templates/engine/annotation.html | 1 - tests/eslintrc.conf.js | 2 - 6 files changed, 111 insertions(+), 340 deletions(-) create mode 100644 cvat/apps/engine/migrations/0010_auto_20181002_1122.py delete mode 100644 cvat/apps/engine/static/engine/js/3rdparty/md5.js diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 033b7a57fadf..96a553885b21 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -519,62 +519,60 @@ def _clamp_poly(self, points, im_size): return ' '.join(verified) - @staticmethod - 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' - ] - - 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) = self._get_values(shape_type) + (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') @@ -606,8 +604,6 @@ def init_from_db(self): 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) @@ -1599,7 +1595,7 @@ def close_root(self): class _XmlAnnotationWriter(_AnnotationWriter): def __init__(self, file): - super().__init__(file, "1.1") + super().__init__(file, "1.0") self.xmlgen = XMLGenerator(self.file, 'utf-8') self._level = 0 diff --git a/cvat/apps/engine/migrations/0010_auto_20181002_1122.py b/cvat/apps/engine/migrations/0010_auto_20181002_1122.py new file mode 100644 index 000000000000..1b84521aa8ea --- /dev/null +++ b/cvat/apps/engine/migrations/0010_auto_20181002_1122.py @@ -0,0 +1,58 @@ +# Generated by Django 2.0.3 on 2018-10-02 08:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0009_auto_20180917_1424'), + ] + + operations = [ + migrations.AddField( + model_name='labeledbox', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='labeledpoints', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='labeledpolygon', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='labeledpolyline', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='objectpath', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='trackedbox', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='trackedpoints', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='trackedpolygon', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='trackedpolyline', + name='client_id', + field=models.IntegerField(default=-1), + ), + ] diff --git a/cvat/apps/engine/static/engine/js/3rdparty/md5.js b/cvat/apps/engine/static/engine/js/3rdparty/md5.js deleted file mode 100644 index 762de3c52012..000000000000 --- a/cvat/apps/engine/static/engine/js/3rdparty/md5.js +++ /dev/null @@ -1,280 +0,0 @@ -/* - * JavaScript MD5 - * https://github.com/blueimp/JavaScript-MD5 - * - * Copyright 2011, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * https://opensource.org/licenses/MIT - * - * Based on - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* global define */ - -;(function ($) { - 'use strict' - - /* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ - function safeAdd (x, y) { - var lsw = (x & 0xffff) + (y & 0xffff) - var msw = (x >> 16) + (y >> 16) + (lsw >> 16) - return (msw << 16) | (lsw & 0xffff) - } - - /* - * Bitwise rotate a 32-bit number to the left. - */ - function bitRotateLeft (num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)) - } - - /* - * These functions implement the four basic operations the algorithm uses. - */ - function md5cmn (q, a, b, x, s, t) { - return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) - } - function md5ff (a, b, c, d, x, s, t) { - return md5cmn((b & c) | (~b & d), a, b, x, s, t) - } - function md5gg (a, b, c, d, x, s, t) { - return md5cmn((b & d) | (c & ~d), a, b, x, s, t) - } - function md5hh (a, b, c, d, x, s, t) { - return md5cmn(b ^ c ^ d, a, b, x, s, t) - } - function md5ii (a, b, c, d, x, s, t) { - return md5cmn(c ^ (b | ~d), a, b, x, s, t) - } - - /* - * Calculate the MD5 of an array of little-endian words, and a bit length. - */ - function binlMD5 (x, len) { - /* append padding */ - x[len >> 5] |= 0x80 << (len % 32) - x[((len + 64) >>> 9 << 4) + 14] = len - - var i - var olda - var oldb - var oldc - var oldd - var a = 1732584193 - var b = -271733879 - var c = -1732584194 - var d = 271733878 - - for (i = 0; i < x.length; i += 16) { - olda = a - oldb = b - oldc = c - oldd = d - - a = md5ff(a, b, c, d, x[i], 7, -680876936) - d = md5ff(d, a, b, c, x[i + 1], 12, -389564586) - c = md5ff(c, d, a, b, x[i + 2], 17, 606105819) - b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330) - a = md5ff(a, b, c, d, x[i + 4], 7, -176418897) - d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426) - c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341) - b = md5ff(b, c, d, a, x[i + 7], 22, -45705983) - a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416) - d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417) - c = md5ff(c, d, a, b, x[i + 10], 17, -42063) - b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162) - a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682) - d = md5ff(d, a, b, c, x[i + 13], 12, -40341101) - c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290) - b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329) - - a = md5gg(a, b, c, d, x[i + 1], 5, -165796510) - d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632) - c = md5gg(c, d, a, b, x[i + 11], 14, 643717713) - b = md5gg(b, c, d, a, x[i], 20, -373897302) - a = md5gg(a, b, c, d, x[i + 5], 5, -701558691) - d = md5gg(d, a, b, c, x[i + 10], 9, 38016083) - c = md5gg(c, d, a, b, x[i + 15], 14, -660478335) - b = md5gg(b, c, d, a, x[i + 4], 20, -405537848) - a = md5gg(a, b, c, d, x[i + 9], 5, 568446438) - d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690) - c = md5gg(c, d, a, b, x[i + 3], 14, -187363961) - b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501) - a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467) - d = md5gg(d, a, b, c, x[i + 2], 9, -51403784) - c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473) - b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734) - - a = md5hh(a, b, c, d, x[i + 5], 4, -378558) - d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463) - c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562) - b = md5hh(b, c, d, a, x[i + 14], 23, -35309556) - a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060) - d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353) - c = md5hh(c, d, a, b, x[i + 7], 16, -155497632) - b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640) - a = md5hh(a, b, c, d, x[i + 13], 4, 681279174) - d = md5hh(d, a, b, c, x[i], 11, -358537222) - c = md5hh(c, d, a, b, x[i + 3], 16, -722521979) - b = md5hh(b, c, d, a, x[i + 6], 23, 76029189) - a = md5hh(a, b, c, d, x[i + 9], 4, -640364487) - d = md5hh(d, a, b, c, x[i + 12], 11, -421815835) - c = md5hh(c, d, a, b, x[i + 15], 16, 530742520) - b = md5hh(b, c, d, a, x[i + 2], 23, -995338651) - - a = md5ii(a, b, c, d, x[i], 6, -198630844) - d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415) - c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905) - b = md5ii(b, c, d, a, x[i + 5], 21, -57434055) - a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571) - d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606) - c = md5ii(c, d, a, b, x[i + 10], 15, -1051523) - b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799) - a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359) - d = md5ii(d, a, b, c, x[i + 15], 10, -30611744) - c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380) - b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649) - a = md5ii(a, b, c, d, x[i + 4], 6, -145523070) - d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379) - c = md5ii(c, d, a, b, x[i + 2], 15, 718787259) - b = md5ii(b, c, d, a, x[i + 9], 21, -343485551) - - a = safeAdd(a, olda) - b = safeAdd(b, oldb) - c = safeAdd(c, oldc) - d = safeAdd(d, oldd) - } - return [a, b, c, d] - } - - /* - * Convert an array of little-endian words to a string - */ - function binl2rstr (input) { - var i - var output = '' - var length32 = input.length * 32 - for (i = 0; i < length32; i += 8) { - output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff) - } - return output - } - - /* - * Convert a raw string to an array of little-endian words - * Characters >255 have their high-byte silently ignored. - */ - function rstr2binl (input) { - var i - var output = [] - output[(input.length >> 2) - 1] = undefined - for (i = 0; i < output.length; i += 1) { - output[i] = 0 - } - var length8 = input.length * 8 - for (i = 0; i < length8; i += 8) { - output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32) - } - return output - } - - /* - * Calculate the MD5 of a raw string - */ - function rstrMD5 (s) { - return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)) - } - - /* - * Calculate the HMAC-MD5, of a key and some data (raw strings) - */ - function rstrHMACMD5 (key, data) { - var i - var bkey = rstr2binl(key) - var ipad = [] - var opad = [] - var hash - ipad[15] = opad[15] = undefined - if (bkey.length > 16) { - bkey = binlMD5(bkey, key.length * 8) - } - for (i = 0; i < 16; i += 1) { - ipad[i] = bkey[i] ^ 0x36363636 - opad[i] = bkey[i] ^ 0x5c5c5c5c - } - hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) - return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)) - } - - /* - * Convert a raw string to a hex string - */ - function rstr2hex (input) { - var hexTab = '0123456789abcdef' - var output = '' - var x - var i - for (i = 0; i < input.length; i += 1) { - x = input.charCodeAt(i) - output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f) - } - return output - } - - /* - * Encode a string as utf-8 - */ - function str2rstrUTF8 (input) { - return unescape(encodeURIComponent(input)) - } - - /* - * Take string arguments and return either raw or hex encoded strings - */ - function rawMD5 (s) { - return rstrMD5(str2rstrUTF8(s)) - } - function hexMD5 (s) { - return rstr2hex(rawMD5(s)) - } - function rawHMACMD5 (k, d) { - return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)) - } - function hexHMACMD5 (k, d) { - return rstr2hex(rawHMACMD5(k, d)) - } - - function md5 (string, key, raw) { - if (!key) { - if (!raw) { - return hexMD5(string) - } - return rawMD5(string) - } - if (!raw) { - return hexHMACMD5(key, string) - } - return rawHMACMD5(key, string) - } - - if (typeof define === 'function' && define.amd) { - define(function () { - return md5 - }) - } else if (typeof module === 'object' && module.exports) { - module.exports = md5 - } else { - $.md5 = md5 - } -})(this) diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index e603468a795e..5a5db26f80ba 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -571,7 +571,7 @@ function uploadAnnotation(shapeCollectionModel, historyModel, annotationParser, try { historyModel.empty(); shapeCollectionModel.empty(); - shapeCollectionModel.import(data, ShapeState.create); + shapeCollectionModel.import(data); shapeCollectionModel.update(); } finally { diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index 33dea2ad459c..9a51776909f3 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -19,7 +19,6 @@ - diff --git a/tests/eslintrc.conf.js b/tests/eslintrc.conf.js index 7771a024f663..7e6950fd7920 100644 --- a/tests/eslintrc.conf.js +++ b/tests/eslintrc.conf.js @@ -83,8 +83,6 @@ module.exports = { 'SELECT_POINT_STROKE_WIDTH': true, // from mousetrap.js 'Mousetrap': true, - // from md5.js - 'md5': true, // from platform.js 'platform': true, // from player.js From a5aefec4d091be7fc446bcf7e1f330ea1b23da63 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Fri, 5 Oct 2018 12:07:21 +0300 Subject: [PATCH 05/12] fixed update_paths function; code cleanup; --- cvat/apps/engine/annotation.py | 22 +++++++++---------- .../static/engine/js/shapeCollection.js | 22 ++++++++++++------- cvat/apps/engine/static/engine/js/shapes.js | 9 +------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 96a553885b21..7b08edfbda24 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -1296,23 +1296,23 @@ def _update_paths_to_db(self): db_path_attrvals.append(db_attrval) path_shapes = shape_to_update.boxes if hasattr(shape_to_update, 'boxes') else shape_to_update.shapes - for shape in path_shapes: + for path_shape in path_shapes: db_shape = shape_class() db_shape.track_id = shape.id 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 + db_shape.xtl = path_shape.xtl + db_shape.ytl = path_shape.ytl + db_shape.xbr = path_shape.xbr + db_shape.ybr = path_shape.ybr else: - db_shape.points = shape.points + db_shape.points = path_shape.points - db_shape.frame = shape.frame - db_shape.occluded = shape.occluded - db_shape.z_order = shape.z_order - db_shape.outside = shape.outside + db_shape.frame = path_shape.frame + db_shape.occluded = path_shape.occluded + db_shape.z_order = path_shape.z_order + db_shape.outside = path_shape.outside - for attr in shape.attributes: + for attr in path_shape.attributes: db_attrspec = self.db_attributes[attr.id] db_attrval = self._get_shape_attr_class(shape_type)() if shape_type == 'polygon_paths': diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index b0bb97e760a1..89519a5b6397 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -7,6 +7,12 @@ /* exported ShapeCollectionModel ShapeCollectionController ShapeCollectionView */ "use strict"; +const ExportType = Object.freeze({ + 'create': 0, + 'update': 1, + 'delete': 2, +}); + class ShapeCollectionModel extends Listener { constructor() { super('onCollectionUpdate', () => this); @@ -227,11 +233,11 @@ class ShapeCollectionModel extends Listener { let shape_container_target = undefined; let export_action_container = undefined; - if (export_type === ShapeState.create) { + if (export_type === ExportType.create) { export_action_container = container.create; - } else if (export_type === ShapeState.update) { + } else if (export_type === ExportType.update) { export_action_container = container.update; - } else if (export_type === ShapeState.delete) { + } else if (export_type === ExportType.delete) { export_action_container = container.delete; } @@ -283,16 +289,16 @@ class ShapeCollectionModel extends Listener { let target_export_container = undefined; if (!shape._removed) { if (!(shape.id in this._initialShapes)) { - target_export_container = this._getExportTargetContainer(ShapeState.create, shape.type, response); + target_export_container = this._getExportTargetContainer(ExportType.create, shape.type, response); } else if (JSON.stringify(this._initialShapes[shape.id]) !== JSON.stringify(shape.export())) { - target_export_container = this._getExportTargetContainer(ShapeState.update, shape.type, response); + target_export_container = this._getExportTargetContainer(ExportType.update, shape.type, response); } else { continue; } } else if (shape.id in this._initialShapes) { // TODO in this case need push only id - target_export_container = this._getExportTargetContainer(ShapeState.delete, shape.type, response); + target_export_container = this._getExportTargetContainer(ExportType.delete, shape.type, response); } else { continue; @@ -360,8 +366,8 @@ class ShapeCollectionModel extends Listener { hasUnsavedChanges() { const exportData = this.export(); - for (const action of ['create', 'update', 'delete']) { - for (const shapes of Object.values(exportData[action])) { + for (const actionType in ExportType) { + for (const shapes of Object.values(exportData[actionType])) { if (shapes.length) { return true; } diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 3bb8fd105407..0c6e2e157ae2 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -15,13 +15,6 @@ const TEXT_MARGIN = 10; /******************************** SHAPE MODELS ********************************/ -const ShapeState = Object.freeze({ - 'nothing': 0, - 'create': 1, - 'update': 2, - 'delete': 3 -}); - class ShapeModel extends Listener { constructor(data, positions, type, id, color) { super('onShapeUpdate', () => this ); @@ -3156,7 +3149,7 @@ class PointsView extends PolyShapeView { function createExportContainer() { const container = {}; - ['create', 'update', 'delete'].forEach( action => { + Object.keys(ExportType).forEach( action => { container[action] = { "boxes": [], "box_paths": [], From 2f26ac720ae4271cd199b84b8b1a629c65dd7c67 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Fri, 5 Oct 2018 15:27:11 +0300 Subject: [PATCH 06/12] fixed import existing tasks without client_id --- cvat/apps/engine/static/engine/js/shapeCollection.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 89519a5b6397..05f6a4ea3417 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -379,6 +379,11 @@ class ShapeCollectionModel extends Listener { updateHash() { this._initialShapes = {}; + + if (this._erased) { + return this; + } + for (const shape of this._shapes) { if (!shape.removed) { this._initialShapes[shape.id] = shape.export(); @@ -400,7 +405,7 @@ class ShapeCollectionModel extends Listener { add(data, type) { let id = null; - if (!('client_id' in data) || data.client_id < 0) { + if (!('client_id' in data)) { id = this._nextIdx(); } else if (data.client_id === -1 ) { From 75958d0cf2f39aad63a4045c7b5bd9c7a45ca226 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 10 Oct 2018 10:15:42 +0300 Subject: [PATCH 07/12] added some sanity checks for cleint_id; improved annotation parser logic; fixed comments from review --- cvat/apps/engine/annotation.py | 79 +++++++++++++++---- .../migrations/0010_auto_20181002_1122.py | 58 -------------- cvat/apps/engine/models.py | 3 +- .../static/engine/js/annotationParser.js | 59 +++++++++++--- .../engine/static/engine/js/annotationUI.js | 2 +- cvat/apps/engine/static/engine/js/base.js | 79 ++++++++++++++++++- .../static/engine/js/shapeCollection.js | 70 ++-------------- cvat/apps/engine/static/engine/js/shapes.js | 21 ----- tests/eslintrc.conf.js | 3 + 9 files changed, 198 insertions(+), 176 deletions(-) delete mode 100644 cvat/apps/engine/migrations/0010_auto_20181002_1122.py diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 7b08edfbda24..ed5f127e239c 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -78,6 +78,7 @@ def save_job(jid, data): db_job = models.Job.objects.select_for_update().get(id=jid) annotation = _AnnotationForJob(db_job) + annotation.validate_data_from_client(data) if data['pre_erase']: annotation.delete_objs_from_db() @@ -359,7 +360,7 @@ def to_boxes(self): group_id=path.group_id, occluded=box.occluded, z_order=box.z_order, - client_id=box.client_id, + client_id=path.client_id, attributes=box.attributes + path.attributes, ) boxes.append(box) @@ -378,7 +379,7 @@ def _to_poly_shapes(self, iter_attr_name): group_id=path.group_id, occluded=shape.occluded, z_order=shape.z_order, - client_id=shape.client_id, + client_id=path.client_id, attributes=shape.attributes + path.attributes, ) shapes.append(shape) @@ -1016,6 +1017,13 @@ def _get_shape_attr_class(self, shape_type): return models.TrackedPointsAttributeVal def _save_paths_to_db(self): + saved_path_ids = list(self.db_job.objectpath_set.values_list('id', 'client_id')) + saved_db_ids = [] + saved_client_ids = [] + for db_id, client_id in saved_path_ids: + saved_db_ids.append(db_id) + saved_client_ids.append(client_id) + for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: db_paths = [] db_path_attrvals = [] @@ -1024,6 +1032,9 @@ def _save_paths_to_db(self): shapes = getattr(self, shape_type) for path in shapes: + if path.client_id in saved_client_ids: + raise Exception('Trying to create new shape with existing client_id {}'.format(path.client_id)) + db_path = models.ObjectPath() db_path.job = self.db_job db_path.label = self.db_labels[path.label.id] @@ -1080,8 +1091,7 @@ def _save_paths_to_db(self): db_shapes.append(db_shape) db_paths.append(db_path) - # in case of sqlite have to store exists ids - db_paths_ids = list(self.db_job.objectpath_set.values_list('id', flat=True)) + db_paths = models.ObjectPath.objects.bulk_create(db_paths) if db_paths and db_paths[0].id == None: @@ -1090,13 +1100,13 @@ def _save_paths_to_db(self): # 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=db_paths_ids).filter(shapes="polygons")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=saved_db_ids)) elif shape_type == 'polyline_paths': - db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="polylines")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=saved_db_ids)) elif shape_type == 'box_paths': - db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="boxes")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=saved_db_ids)) elif shape_type == 'points_paths': - db_paths = list(self.db_job.objectpath_set.exclude(id__in=db_paths_ids).filter(shapes="points")) + db_paths = list(self.db_job.objectpath_set.exclude(id__in=saved_db_ids)) for db_attrval in db_path_attrvals: db_attrval.track_id = db_paths[db_attrval.track_id].id @@ -1145,8 +1155,19 @@ def _save_shapes_to_db(self): db_shapes = [] db_attrvals = [] + saved_shapes_ids = list(self._get_shape_class(shape_type).objects.filter(job_id=self.db_job.id).values_list('id', 'client_id')) + saved_client_ids = [] + saved_db_ids = [] + + for db_id, client_id in saved_shapes_ids: + saved_db_ids.append(db_id) + saved_client_ids.append(client_id) + shapes = getattr(self, shape_type) for shape in shapes: + if shape.client_id in saved_client_ids: + raise Exception('Trying to create new shape with existing client_id {}'.format(shape.client_id)) + db_shape = self._get_shape_class(shape_type)() db_shape.job = self.db_job db_shape.label = self.db_labels[shape.label.id] @@ -1180,7 +1201,6 @@ def _save_shapes_to_db(self): db_shapes.append(db_shape) - db_shapes_ids = list(self._get_shape_class(shape_type).objects.filter(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: @@ -1188,7 +1208,7 @@ def _save_shapes_to_db(self): # 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=db_shapes_ids)) + db_shapes = list(self._get_shape_set(shape_type).exclude(id__in=saved_db_ids)) for db_attrval in db_attrvals: @@ -1203,8 +1223,7 @@ def _save_shapes_to_db(self): self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) - def _get_relaited_obj_field_name(self, shape_type): - 'polygon_paths', 'polyline_paths', 'points_paths', 'box_paths' + def _get_related_obj_field_name(self, shape_type): if shape_type == 'boxes' or shape_type == 'box_paths': return 'box' elif shape_type == 'points' or shape_type == 'points_paths': @@ -1223,7 +1242,7 @@ def _update_shapes_to_db(self): client_ids_to_update = list(shapes_to_update.keys()) - attr_filter = {'{}__client_id__in'.format(self._get_relaited_obj_field_name(shape_type)):client_ids_to_update} + attr_filter = {'{}__client_id__in'.format(self._get_related_obj_field_name(shape_type)):client_ids_to_update} self._get_shape_attr_class(shape_type).objects.filter(**attr_filter).delete() for shape_id_to_update, shape_to_update in shapes_to_update.items(): @@ -1265,11 +1284,11 @@ def _update_paths_to_db(self): db_shapes = [] db_shape_attrvals = [] - shapes_to_update = {shape.client_id: shape for shape in getattr(self, shape_type)} - if not shapes_to_update: + paths_for_update = {shape.client_id: shape for shape in getattr(self, shape_type)} + if not paths_for_update: continue - client_ids_to_update = list(shapes_to_update.keys()) + client_ids_to_update = list(paths_for_update.keys()) if None in client_ids_to_update: raise Exception('Trying to update None id') @@ -1280,7 +1299,7 @@ def _update_paths_to_db(self): shape_class.objects.filter(track__job__id=self.db_job.id, track__client_id__in=client_ids_to_update).delete() # update shape props - for shape_id_to_update, shape_to_update in shapes_to_update.items(): + for shape_id_to_update, shape_to_update in paths_for_update.items(): shape = models.ObjectPath.objects.get(job_id=self.db_job.id, client_id=shape_id_to_update) shape.label = self.db_labels[shape_to_update.label.id] shape.frame = shape_to_update.frame @@ -1476,6 +1495,32 @@ def to_client(self): return data + def validate_data_from_client(self, data): + # check unique id for each object + client_ids = set() + def extract_and_check_clinet_id(obj): + if 'client_id' not in box: + raise Exception('No client_id field in received data') + client_id = obj['client_id'] + if obj['client_id'] in client_ids: + raise Exception('More than one object has the same client_id {}'.format(client_id)) + client_ids.add(client_id) + return client_id + + for action in ['create', 'update', 'delete']: + for box in data[action]['boxes']: + extract_and_check_clinet_id(box) + + for poly_shape_type in ['points', 'polygons', 'polylines']: + for poly_shape in data[action][poly_shape_type]: + extract_and_check_clinet_id(box) + + for path in data[action]['box_paths']: + extract_and_check_clinet_id(path) + + for poly_path_type in ['points_paths', 'polygon_paths', 'polyline_paths']: + for path in data[action][poly_path_type]: + extract_and_check_clinet_id(path) class _AnnotationForSegment(_Annotation): def __init__(self, db_segment): diff --git a/cvat/apps/engine/migrations/0010_auto_20181002_1122.py b/cvat/apps/engine/migrations/0010_auto_20181002_1122.py deleted file mode 100644 index 1b84521aa8ea..000000000000 --- a/cvat/apps/engine/migrations/0010_auto_20181002_1122.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 2.0.3 on 2018-10-02 08:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('engine', '0009_auto_20180917_1424'), - ] - - operations = [ - migrations.AddField( - model_name='labeledbox', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='labeledpoints', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='labeledpolygon', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='labeledpolyline', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='objectpath', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='trackedbox', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='trackedpoints', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='trackedpolygon', - name='client_id', - field=models.IntegerField(default=-1), - ), - migrations.AddField( - model_name='trackedpolyline', - name='client_id', - field=models.IntegerField(default=-1), - ), - ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 8b21974c2162..a9fd724c2571 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -139,13 +139,13 @@ class Annotation(models.Model): label = models.ForeignKey(Label, on_delete=models.CASCADE) frame = models.PositiveIntegerField() group_id = models.PositiveIntegerField(default=0) + client_id = models.IntegerField(default=-1) class Meta: abstract = True class Shape(models.Model): occluded = models.BooleanField(default=False) z_order = models.IntegerField(default=0) - client_id = models.IntegerField(default=-1) class Meta: abstract = True @@ -190,7 +190,6 @@ class LabeledPointsAttributeVal(AttributeVal): class ObjectPath(Annotation): id = models.BigAutoField(primary_key=True) - client_id = models.IntegerField(default=-1) shapes = models.CharField(max_length=10, default='boxes') class ObjectPathAttributeVal(AttributeVal): diff --git a/cvat/apps/engine/static/engine/js/annotationParser.js b/cvat/apps/engine/static/engine/js/annotationParser.js index f90967d8e9b9..3af8511c685f 100644 --- a/cvat/apps/engine/static/engine/js/annotationParser.js +++ b/cvat/apps/engine/static/engine/js/annotationParser.js @@ -15,7 +15,7 @@ class AnnotationParser { this._flipped = job.flipped; this._im_meta = job.image_meta_data; this._labelsInfo = labelsInfo; - this.counter = 0; + this._client_id_set = new Set(); } _xmlParseError(parsedXML) { @@ -52,8 +52,7 @@ class AnnotationParser { let occluded = +box.getAttribute('occluded'); let z_order = box.getAttribute('z_order') || '0'; - let client_id = box.getAttribute('client_id') || '-1' - return [xtl, ytl, xbr, ybr, occluded, +z_order, +client_id]; + return [xtl, ytl, xbr, ybr, occluded, +z_order]; } _getPolyPosition(shape, frame) { @@ -79,8 +78,7 @@ class AnnotationParser { let occluded = +shape.getAttribute('occluded'); let z_order = shape.getAttribute('z_order') || '0'; - let client_id = box.getAttribute('client_id') || '-1' - return [points, occluded, +z_order, +client_id]; + return [points, occluded, +z_order]; } _getAttribute(labelId, attrTag) { @@ -134,7 +132,8 @@ class AnnotationParser { let result = []; for (let track of tracks) { let label = track.getAttribute('label'); - let group_id = track.getAttribute('group_id') || "0"; + let group_id = track.getAttribute('group_id') || '0'; + let client_id = track.getAttribute('client_id') || '-1'; let labelId = this._labelsInfo.labelIdOf(label); if (labelId === null) { throw Error(`An unknown label found in the annotation file: ${label}`); @@ -152,6 +151,7 @@ class AnnotationParser { !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) { shapes[0].setAttribute('label', label); shapes[0].setAttribute('group_id', group_id); + shapes[0].setAttribute('client_id', client_id); result.push(shapes[0]); } } @@ -160,6 +160,17 @@ class AnnotationParser { return result; } + _updateClientIds(data) { + let maxId = Math.max(-1, ...Array.from(this._client_id_set)); + for (const shape_type in data) { + for (const shape of data[shape_type]) { + if (shape.client_id === -1) { + shape.client_id = ++maxId; + } + } + } + } + _parseAnnotationData(xml) { let data = { boxes: [], @@ -212,10 +223,19 @@ class AnnotationParser { throw Error('An unknown label found in the annotation file: ' + shape.getAttribute('label')); } + let client_id = parseInt(shape.getAttribute('client_id') || '-1'); + if (client_id !== -1) { + if (this._client_id_set.has(client_id)) { + throw Error('More than one shape has the same client_id attribute'); + } + + this._client_id_set.add(client_id); + } + let attributeList = this._getAttributeList(shape, labelId); if (shape_type === 'boxes') { - let [xtl, ytl, xbr, ybr, occluded, z_order, client_id] = this._getBoxPosition(shape, frame); + let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, frame); data.boxes.push({ label_id: labelId, group_id: +groupId, @@ -227,7 +247,7 @@ class AnnotationParser { ybr: ybr, z_order: z_order, attributes: attributeList, - client_id: client_id !== -1 ? client_id : this.counter++, + client_id: client_id, }); } else { @@ -240,7 +260,7 @@ class AnnotationParser { occluded: occluded, z_order: z_order, attributes: attributeList, - client_id: client_id !== -1 ? client_id : this.counter++, + client_id: client_id, }); } } @@ -261,7 +281,7 @@ class AnnotationParser { for (let track of tracks) { let labelId = this._labelsInfo.labelIdOf(track.getAttribute('label')); let groupId = track.getAttribute('group_id') || '0'; - let client_id = track.getAttribute('client_id') || '-1'; + let client_id = parseInt(track.getAttribute('client_id') || '-1'); if (labelId === null) { throw Error('An unknown label found in the annotation file: ' + name); } @@ -314,9 +334,17 @@ class AnnotationParser { frame: +parsed[type][0].getAttribute('frame'), attributes: [], shapes: [], - client_id: client_id !== -1 ? client_id : this.counter++, + client_id: client_id, }; + if (client_id !== -1) { + if (this._client_id_set.has(client_id)) { + throw Error('More than one shape has the same client_id attribute'); + } + + this._client_id_set.add(client_id); + } + for (let shape of parsed[type]) { let keyFrame = +shape.getAttribute('keyframe'); let outside = +shape.getAttribute('outside'); @@ -388,7 +416,12 @@ class AnnotationParser { return data; } + _reset() { + this._client_id_set.clear(); + } + parse(text) { + this._reset(); let xml = this._parser.parseFromString(text, 'text/xml'); let parseerror = this._xmlParseError(xml); if (parseerror.length) { @@ -397,6 +430,8 @@ class AnnotationParser { let interpolationData = this._parseInterpolationData(xml); let annotationData = this._parseAnnotationData(xml); - return Object.assign({}, annotationData, interpolationData); + let data = Object.assign({}, annotationData, interpolationData); + this._updateClientIds(data); + return data; } } diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 5a5db26f80ba..d389382af92b 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -610,7 +610,6 @@ function saveAnnotation(shapeCollectionModel, job) { }); const exportedData = shapeCollectionModel.export(); - shapeCollectionModel.reset_state(); const annotationLogs = Logger.getLogs(); const data = { @@ -623,6 +622,7 @@ function saveAnnotation(shapeCollectionModel, job) { saveJobRequest(job.jobid, data, () => { // success + shapeCollectionModel.reset_state(); shapeCollectionModel.updateHash(); saveButton.text('Success!'); setTimeout(() => { diff --git a/cvat/apps/engine/static/engine/js/base.js b/cvat/apps/engine/static/engine/js/base.js index dc9fb1ef2836..7de048ed25fc 100644 --- a/cvat/apps/engine/static/engine/js/base.js +++ b/cvat/apps/engine/static/engine/js/base.js @@ -4,7 +4,10 @@ * SPDX-License-Identifier: MIT */ -/* exported confirm showMessage showOverlay dumpAnnotationRequest */ +/* exported confirm showMessage showOverlay dumpAnnotationRequest ExportType + createExportContainer getExportTargetContainer +*/ + "use strict"; Math.clamp = function(x, min, max) { @@ -160,6 +163,80 @@ function dumpAnnotationRequest(dumpButton, taskID) { } } +const ExportType = Object.freeze({ + 'create': 0, + 'update': 1, + 'delete': 2, +}); + +function createExportContainer() { + const container = {}; + Object.keys(ExportType).forEach( action => { + container[action] = { + "boxes": [], + "box_paths": [], + "points": [], + "points_paths": [], + "polygons": [], + "polygon_paths": [], + "polylines": [], + "polyline_paths": [], + }; + }); + container.pre_erase = false; + + return container; +} + +function getExportTargetContainer(export_type, shape_type, container) { + let shape_container_target = undefined; + let export_action_container = undefined; + + switch (export_type) { + case ExportType.create: + export_action_container = container.create; + break; + case ExportType.update: + export_action_container = container.update; + break; + case ExportType.delete: + export_action_container = container.delete; + break; + default: + throw Error('Unexpected export type'); + } + + switch (shape_type) { + case 'annotation_box': + shape_container_target = export_action_container.boxes; + break; + case 'interpolation_box': + shape_container_target = export_action_container.box_paths; + break; + case 'annotation_points': + shape_container_target = export_action_container.points; + break; + case 'interpolation_points': + shape_container_target = export_action_container.points_paths; + break; + case 'annotation_polygon': + shape_container_target = export_action_container.polygons; + break; + case 'interpolation_polygon': + shape_container_target = export_action_container.polygon_paths; + break; + case 'annotation_polyline': + shape_container_target = export_action_container.polylines; + break; + case 'interpolation_polyline': + shape_container_target = export_action_container.polyline_paths; + break; + default: + throw Error('Undefined shape type'); + } + + return shape_container_target; +} /* These HTTP methods do not require CSRF protection */ function csrfSafeMethod(method) { diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 05f6a4ea3417..155a3c2fdeda 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -7,12 +7,6 @@ /* exported ShapeCollectionModel ShapeCollectionController ShapeCollectionView */ "use strict"; -const ExportType = Object.freeze({ - 'create': 0, - 'update': 1, - 'delete': 2, -}); - class ShapeCollectionModel extends Listener { constructor() { super('onCollectionUpdate', () => this); @@ -58,7 +52,7 @@ class ShapeCollectionModel extends Listener { this._filter = new FilterModel(() => this.update()); this._splitter = new ShapeSplitter(); this._erased = false; - this._initialShapes = []; + this._initialShapes = {}; } _nextIdx() { @@ -229,54 +223,6 @@ class ShapeCollectionModel extends Listener { return this; } - _getExportTargetContainer(export_type, shape_type, container) { - let shape_container_target = undefined; - let export_action_container = undefined; - - if (export_type === ExportType.create) { - export_action_container = container.create; - } else if (export_type === ExportType.update) { - export_action_container = container.update; - } else if (export_type === ExportType.delete) { - export_action_container = container.delete; - } - - if (!export_action_container) { - throw Error('Undefined action of shape'); - } - - switch (shape_type) { - case 'annotation_box': - shape_container_target = export_action_container.boxes; - break; - case 'interpolation_box': - shape_container_target = export_action_container.box_paths; - break; - case 'annotation_points': - shape_container_target = export_action_container.points; - break; - case 'interpolation_points': - shape_container_target = export_action_container.points_paths; - break; - case 'annotation_polygon': - shape_container_target = export_action_container.polygons; - break; - case 'interpolation_polygon': - shape_container_target = export_action_container.polygon_paths; - break; - case 'annotation_polyline': - shape_container_target = export_action_container.polylines; - break; - case 'interpolation_polyline': - shape_container_target = export_action_container.polyline_paths; - } - - if (!shape_container_target) { - throw Error('Undefined shape type'); - } - return shape_container_target; - } - reset_state() { this._erased = false; } @@ -288,17 +234,17 @@ class ShapeCollectionModel extends Listener { for (const shape of this._shapes) { let target_export_container = undefined; if (!shape._removed) { - if (!(shape.id in this._initialShapes)) { - target_export_container = this._getExportTargetContainer(ExportType.create, shape.type, response); + if (!(shape.id in this._initialShapes) || this._erased) { + target_export_container = getExportTargetContainer(ExportType.create, shape.type, response); } else if (JSON.stringify(this._initialShapes[shape.id]) !== JSON.stringify(shape.export())) { - target_export_container = this._getExportTargetContainer(ExportType.update, shape.type, response); + target_export_container = getExportTargetContainer(ExportType.update, shape.type, response); } else { continue; } } - else if (shape.id in this._initialShapes) { + else if (shape.id in this._initialShapes && !this._erased) { // TODO in this case need push only id - target_export_container = this._getExportTargetContainer(ExportType.delete, shape.type, response); + target_export_container = getExportTargetContainer(ExportType.delete, shape.type, response); } else { continue; @@ -878,10 +824,6 @@ class ShapeCollectionModel extends Listener { get shapes() { return this._shapes; } - - get erased() { - return this._erased; - } } class ShapeCollectionController { diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 0c6e2e157ae2..f8a6f7b3d830 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -3147,27 +3147,6 @@ class PointsView extends PolyShapeView { } } -function createExportContainer() { - const container = {}; - Object.keys(ExportType).forEach( action => { - container[action] = { - "boxes": [], - "box_paths": [], - "points": [], - "points_paths": [], - "polygons": [], - "polygon_paths": [], - "polylines": [], - "polyline_paths": [], - }; - }); - container.pre_erase = false; - - return container; -} - - - function buildShapeModel(data, type, idx, color) { switch (type) { case 'interpolation_box': diff --git a/tests/eslintrc.conf.js b/tests/eslintrc.conf.js index 7e6950fd7920..99fcaf298576 100644 --- a/tests/eslintrc.conf.js +++ b/tests/eslintrc.conf.js @@ -49,6 +49,9 @@ module.exports = { 'showOverlay': true, 'confirm': true, 'dumpAnnotationRequest': true, + 'createExportContainer': true, + 'ExportType': true, + 'getExportTargetContainer': true, // from shapeCollection.js 'ShapeCollectionModel': true, 'ShapeCollectionController': true, From 9c8d1852b42ce3640d68adf78b0435214b343507 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 10 Oct 2018 10:31:18 +0300 Subject: [PATCH 08/12] added missed migration --- .../migrations/0010_auto_20181009_0938.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 cvat/apps/engine/migrations/0010_auto_20181009_0938.py diff --git a/cvat/apps/engine/migrations/0010_auto_20181009_0938.py b/cvat/apps/engine/migrations/0010_auto_20181009_0938.py new file mode 100644 index 000000000000..ddc2fee327cb --- /dev/null +++ b/cvat/apps/engine/migrations/0010_auto_20181009_0938.py @@ -0,0 +1,38 @@ +# Generated by Django 2.0.3 on 2018-10-09 06:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0009_auto_20180917_1424'), + ] + + operations = [ + migrations.AddField( + model_name='labeledbox', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='labeledpoints', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='labeledpolygon', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='labeledpolyline', + name='client_id', + field=models.IntegerField(default=-1), + ), + migrations.AddField( + model_name='objectpath', + name='client_id', + field=models.IntegerField(default=-1), + ), + ] From 7c2cdaaa8520b8b4314664a760be620f98374f10 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 10 Oct 2018 17:12:40 +0300 Subject: [PATCH 09/12] fixed comments --- cvat/apps/engine/annotation.py | 35 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 3751fdd54575..258959a5f9b6 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -1498,29 +1498,22 @@ def to_client(self): def validate_data_from_client(self, data): # check unique id for each object client_ids = set() - def extract_and_check_clinet_id(obj): - if 'client_id' not in box: - raise Exception('No client_id field in received data') - client_id = obj['client_id'] - if obj['client_id'] in client_ids: + def extract_and_check_clinet_id(shape): + if 'client_id' not in shape: + raise Exception('No client_id field in received data') + client_id = shape['client_id'] + if client_id in client_ids: raise Exception('More than one object has the same client_id {}'.format(client_id)) client_ids.add(client_id) return client_id - for action in ['create', 'update', 'delete']: - for box in data[action]['boxes']: - extract_and_check_clinet_id(box) - - for poly_shape_type in ['points', 'polygons', 'polylines']: - for poly_shape in data[action][poly_shape_type]: - extract_and_check_clinet_id(box) + shape_types = ['boxes', 'points', 'polygons', 'polylines', 'box_paths', + 'points_paths', 'polygon_paths', 'polyline_paths'] - for path in data[action]['box_paths']: - extract_and_check_clinet_id(path) - - for poly_path_type in ['points_paths', 'polygon_paths', 'polyline_paths']: - for path in data[action][poly_path_type]: - extract_and_check_clinet_id(path) + for action in ['create', 'update', 'delete']: + for shape_type in shape_types: + for shape in data[action][shape_type]: + extract_and_check_clinet_id(shape) class _AnnotationForSegment(_Annotation): def __init__(self, db_segment): @@ -2066,7 +2059,7 @@ def _flip_shape(shape, im_w, im_h): ("xbr", "{:.2f}".format(shape.xbr)), ("ybr", "{:.2f}".format(shape.ybr)), ("occluded", str(int(shape.occluded))), - ("client_id", "{}".format(shape.client_id)), + ("client_id", str(shape.client_id)), ]) if db_task.z_order: dump_dict['z_order'] = str(shape.z_order) @@ -2086,7 +2079,7 @@ def _flip_shape(shape, im_w, im_h): )) for p in shape.points.split(' ')) )), ("occluded", str(int(shape.occluded))), - ("client_id", "{}".format(shape.client_id)), + ("client_id", str(shape.client_id)), ]) if db_task.z_order: @@ -2134,7 +2127,7 @@ def _flip_shape(shape, im_w, im_h): dump_dict = OrderedDict([ ("id", str(path_idx)), ("label", path.label.name), - ("client_id", "{}".format(path.client_id)), + ("client_id", str(path.client_id)), ]) if path.group_id: dump_dict['group_id'] = str(path.group_id) From 83a97a9ea3f92df3758dc4367c995f5bf129d052 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 11 Oct 2018 12:57:28 +0300 Subject: [PATCH 10/12] changed logic of update shapes method --- cvat/apps/engine/annotation.py | 171 +++------------------------------ 1 file changed, 15 insertions(+), 156 deletions(-) diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 258959a5f9b6..839e428de3e6 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -1223,170 +1223,29 @@ def _save_shapes_to_db(self): self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) - def _get_related_obj_field_name(self, shape_type): - if shape_type == 'boxes' or shape_type == 'box_paths': - return 'box' - elif shape_type == 'points' or shape_type == 'points_paths': - return 'points' - elif shape_type == 'polygons' or shape_type == 'polygon_paths': - return 'polygon' - elif shape_type == 'polylines' or shape_type == 'polyline_paths': - return 'polyline' - - def _update_shapes_to_db(self): - for shape_type in ['boxes', 'points', 'polygons', 'polylines']: - db_attrvals = [] - shapes_to_update = {shape.client_id: shape for shape in getattr(self, shape_type)} - if not shapes_to_update: - continue - - client_ids_to_update = list(shapes_to_update.keys()) - - attr_filter = {'{}__client_id__in'.format(self._get_related_obj_field_name(shape_type)):client_ids_to_update} - self._get_shape_attr_class(shape_type).objects.filter(**attr_filter).delete() - - for shape_id_to_update, shape_to_update in shapes_to_update.items(): - shape = self._get_shape_class(shape_type).objects.get(job_id=self.db_job.id, client_id=shape_id_to_update) - shape.label = self.db_labels[shape_to_update.label.id] - shape.group_id = shape_to_update.group_id - shape.frame = shape_to_update.frame - shape.occluded = shape_to_update.occluded - shape.z_order = shape_to_update.z_order - if shape_type == 'boxes': - shape.xtl = shape_to_update.xtl - shape.ytl = shape_to_update.ytl - shape.xbr = shape_to_update.xbr - shape.ybr = shape_to_update.ybr - else: - shape.points = shape_to_update.points - shape.save() - - for attr in shape_to_update.attributes: - db_attrval = self._get_shape_attr_class(shape_type)() - if shape_type == 'polygons': - db_attrval.polygon_id = shape.id - elif shape_type == 'polylines': - db_attrval.polyline_id = shape.id - elif shape_type == 'boxes': - db_attrval.box_id = shape.id - else: - db_attrval.points_id = shape.id - - db_attrval.spec = self.db_attributes[attr.id] - db_attrval.value = attr.value - db_attrvals.append(db_attrval) - - self._get_shape_attr_class(shape_type).objects.bulk_create(db_attrvals) - - def _update_paths_to_db(self): - for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: - db_path_attrvals = [] - db_shapes = [] - db_shape_attrvals = [] - - paths_for_update = {shape.client_id: shape for shape in getattr(self, shape_type)} - if not paths_for_update: - continue - - client_ids_to_update = list(paths_for_update.keys()) - - if None in client_ids_to_update: - raise Exception('Trying to update None id') + def _update_shapes_in_db(self): + self._delete_paths_from_db() + self._save_paths_to_db() - models.ObjectPathAttributeVal.objects.filter(track__job__id=self.db_job.id, track__client_id__in=client_ids_to_update).delete() - - shape_class = self._get_shape_class(shape_type) - shape_class.objects.filter(track__job__id=self.db_job.id, track__client_id__in=client_ids_to_update).delete() - - # update shape props - for shape_id_to_update, shape_to_update in paths_for_update.items(): - shape = models.ObjectPath.objects.get(job_id=self.db_job.id, client_id=shape_id_to_update) - shape.label = self.db_labels[shape_to_update.label.id] - shape.frame = shape_to_update.frame - shape.group_id = shape_to_update.group_id - shape.save() - - for attr in shape_to_update.attributes: - db_attrspec = self.db_attributes[attr.id] - db_attrval = models.ObjectPathAttributeVal() - db_attrval.track_id = shape.id - db_attrval.spec = db_attrspec - db_attrval.value = attr.value - db_path_attrvals.append(db_attrval) - - path_shapes = shape_to_update.boxes if hasattr(shape_to_update, 'boxes') else shape_to_update.shapes - for path_shape in path_shapes: - db_shape = shape_class() - db_shape.track_id = shape.id - if shape_type == 'box_paths': - db_shape.xtl = path_shape.xtl - db_shape.ytl = path_shape.ytl - db_shape.xbr = path_shape.xbr - db_shape.ybr = path_shape.ybr - else: - db_shape.points = path_shape.points - - db_shape.frame = path_shape.frame - db_shape.occluded = path_shape.occluded - db_shape.z_order = path_shape.z_order - db_shape.outside = path_shape.outside - - for attr in path_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) - - models.ObjectPathAttributeVal.objects.bulk_create(db_path_attrvals) - - 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.filter(track__job_id=self.db_job.id).exclude(id__in=db_shapes_ids)) - - 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 _update_paths_in_db(self): + self._delete_shapes_from_db() + self._save_shapes_to_db() def _delete_shapes_from_db(self): for shape_type in ['polygons', 'polylines', 'points', 'boxes']: - db_ids_to_delete = list(shape.client_id for shape in getattr(self, shape_type)) - deleted = self._get_shape_set(shape_type).filter(client_id__in=db_ids_to_delete).delete() + client_ids_to_delete = list(shape.client_id for shape in getattr(self, 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(db_ids_to_delete) == 0) and (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): + 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): for shape_type in ['polygon_paths', 'polyline_paths', 'points_paths', 'box_paths']: - db_ids_to_delete = list(shape.client_id for shape in getattr(self, shape_type)) - deleted = self.db_job.objectpath_set.filter(client_id__in=db_ids_to_delete).delete() + client_ids_to_delete = list(shape.client_id for shape in getattr(self, 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(db_ids_to_delete) == 0) and \ - (class_name in deleted[1] and deleted[1][class_name] != len(db_ids_to_delete)): + 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): @@ -1401,8 +1260,8 @@ def save_to_db(self, action): self._save_shapes_to_db() self._save_paths_to_db() elif action == 'update': - self._update_shapes_to_db() - self._update_paths_to_db() + self._update_shapes_in_db() + self._update_paths_in_db() elif action == 'delete': self._delete_shapes_from_db() self._delete_paths_from_db() From a7c74c4bca7baf28a67ab66cf6b8094b9d6bf30e Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 11 Oct 2018 16:49:55 +0300 Subject: [PATCH 11/12] fixed dumping and uploading annotation if annotation task contains interpolation shapes --- cvat/apps/engine/annotation.py | 80 ++++++++++++++----- ...009_0938.py => 0010_auto_20181011_1517.py} | 12 +-- cvat/apps/engine/models.py | 2 +- 3 files changed, 66 insertions(+), 28 deletions(-) rename cvat/apps/engine/migrations/{0010_auto_20181009_0938.py => 0010_auto_20181011_1517.py} (69%) diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 839e428de3e6..e4c5f2679f63 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -201,8 +201,8 @@ def __init__(self, label, points, frame, group_id, occluded, z_order, client_id= self.group_id = group_id class _TrackedPolyShape(_PolyShape): - def __init__(self, points, frame, occluded, z_order, outside, client_id=None, attributes=None): - super().__init__(points, frame, occluded, z_order, client_id, attributes) + 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): @@ -347,8 +347,33 @@ def reset(self): 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): + def to_boxes(self, start_client_id): boxes = [] for path in self.box_paths: for box in path.get_interpolated_boxes(): @@ -360,14 +385,15 @@ def to_boxes(self): group_id=path.group_id, occluded=box.occluded, z_order=box.z_order, - client_id=path.client_id, + client_id=start_client_id, attributes=box.attributes + path.attributes, ) boxes.append(box) + start_client_id += 1 - return self.boxes + boxes + return self.boxes + boxes, start_client_id - def _to_poly_shapes(self, iter_attr_name): + 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(): @@ -379,20 +405,24 @@ def _to_poly_shapes(self, iter_attr_name): group_id=path.group_id, occluded=shape.occluded, z_order=shape.z_order, - client_id=path.client_id, + client_id=start_client_id, attributes=shape.attributes + path.attributes, ) shapes.append(shape) - return shapes + start_client_id += 1 + return shapes, start_client_id - def to_polygons(self): - return self._to_poly_shapes('polygon_paths') + self.polygons + 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): - return self._to_poly_shapes('polyline_paths') + self.polylines + 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): - return self._to_poly_shapes('points_paths') + self.points + 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 = [] @@ -769,7 +799,13 @@ def get_values(shape_type): 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, db_path.frame, self.stop_frame, db_path.group_id, db_path.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.id, + ) for db_attr in db_path.attributes: spec = self.db_attributes[db_attr.spec_id] attr = _Attribute(spec, db_attr.value) @@ -783,7 +819,6 @@ def get_values(shape_type): occluded=db_shape.occluded, z_order=db_shape.z_order, outside=db_shape.outside, - client_id=db_shape.client_id, ) assert shape.frame > frame frame = shape.frame @@ -1861,23 +1896,26 @@ def _flip_shape(shape, im_w, im_h): shapes["polygons"] = {} shapes["polylines"] = {} shapes["points"] = {} - - for box in self.to_boxes(): + 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) - for polygon in self.to_polygons(): + 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) - for polyline in self.to_polylines(): + 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) - for points in self.to_points(): + 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) diff --git a/cvat/apps/engine/migrations/0010_auto_20181009_0938.py b/cvat/apps/engine/migrations/0010_auto_20181011_1517.py similarity index 69% rename from cvat/apps/engine/migrations/0010_auto_20181009_0938.py rename to cvat/apps/engine/migrations/0010_auto_20181011_1517.py index ddc2fee327cb..c33d1319c0f4 100644 --- a/cvat/apps/engine/migrations/0010_auto_20181009_0938.py +++ b/cvat/apps/engine/migrations/0010_auto_20181011_1517.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-10-09 06:38 +# Generated by Django 2.0.9 on 2018-10-11 12:17 from django.db import migrations, models @@ -13,26 +13,26 @@ class Migration(migrations.Migration): migrations.AddField( model_name='labeledbox', name='client_id', - field=models.IntegerField(default=-1), + field=models.BigIntegerField(default=-1), ), migrations.AddField( model_name='labeledpoints', name='client_id', - field=models.IntegerField(default=-1), + field=models.BigIntegerField(default=-1), ), migrations.AddField( model_name='labeledpolygon', name='client_id', - field=models.IntegerField(default=-1), + field=models.BigIntegerField(default=-1), ), migrations.AddField( model_name='labeledpolyline', name='client_id', - field=models.IntegerField(default=-1), + field=models.BigIntegerField(default=-1), ), migrations.AddField( model_name='objectpath', name='client_id', - field=models.IntegerField(default=-1), + field=models.BigIntegerField(default=-1), ), ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index a9fd724c2571..608cdafb1e30 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -139,7 +139,7 @@ class Annotation(models.Model): label = models.ForeignKey(Label, on_delete=models.CASCADE) frame = models.PositiveIntegerField() group_id = models.PositiveIntegerField(default=0) - client_id = models.IntegerField(default=-1) + client_id = models.BigIntegerField(default=-1) class Meta: abstract = True From a5bc4d2ff8933615e61d5ecc580038f6ae01361b Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Mon, 15 Oct 2018 16:12:47 +0300 Subject: [PATCH 12/12] preparation for reid --- cvat/apps/engine/static/engine/js/annotationUI.js | 2 +- cvat/apps/engine/static/engine/js/shapeCollection.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 1f16b9e1c23e..9f0ca3acb5e1 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -105,7 +105,7 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) { let shapeCollectionView = new ShapeCollectionView(shapeCollectionModel, shapeCollectionController); window.cvat.data = { - get: () => shapeCollectionModel.export(), + get: () => shapeCollectionModel.exportAll(), set: (data) => { shapeCollectionModel.empty(); shapeCollectionModel.import(data); diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 66a5fe849b18..7f6416f866d9 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -271,6 +271,16 @@ class ShapeCollectionModel extends Listener { return response; } + exportAll() { + const response = createExportContainer(); + for (const shape of this._shapes) { + if (!shape._removed) { + getExportTargetContainer(ExportType.create, shape.type, response).push(shape.export()); + } + } + return response.create; + } + find(direction) { if (Math.sign(direction) > 0) { let frame = this._frame + 1;