From 9608b7d237067762b28f8a5c88a094d9885d33a4 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov <41117609+azhavoro@users.noreply.github.com> Date: Mon, 26 Nov 2018 19:11:58 +0300 Subject: [PATCH] Remove client id info from dump file (#213) * removed shape id info from dump * force set client id by server in save annotation for task functionality * delete unused code --- .../static/dashboard/js/dashboard.js | 13 ++- cvat/apps/engine/annotation.py | 110 +++++++++--------- .../migrations/0014_job_max_shape_id.py | 18 +++ cvat/apps/engine/models.py | 1 + .../static/engine/js/annotationParser.js | 35 +----- .../engine/static/engine/js/annotationUI.js | 10 +- cvat/apps/engine/static/engine/js/base.js | 36 +++++- .../static/engine/js/shapeCollection.js | 27 ++--- cvat/apps/engine/task.py | 11 +- cvat/apps/tf_annotation/views.py | 9 +- 10 files changed, 146 insertions(+), 124 deletions(-) create mode 100644 cvat/apps/engine/migrations/0014_job_max_shape_id.py diff --git a/cvat/apps/dashboard/static/dashboard/js/dashboard.js b/cvat/apps/dashboard/static/dashboard/js/dashboard.js index 07893ee3..091ed668 100644 --- a/cvat/apps/dashboard/static/dashboard/js/dashboard.js +++ b/cvat/apps/dashboard/static/dashboard/js/dashboard.js @@ -525,11 +525,14 @@ function uploadAnnotationRequest() { url: '/get/task/' + window.cvat.dashboard.taskID, success: function(data) { let annotationParser = new AnnotationParser({ - start: 0, - stop: data.size, - image_meta_data: data.image_meta_data, - flipped: data.flipped - }, new LabelsInfo(data.spec)); + start: 0, + stop: data.size, + image_meta_data: data.image_meta_data, + flipped: data.flipped + }, + new LabelsInfo(data.spec), + new ConstIdGenerator(-1), + ); let asyncParse = function() { let parsed = null; diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 2c8e2471..b5dc93a0 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -82,7 +82,8 @@ def save_job(jid, data): .select_for_update().get(id=jid) annotation = _AnnotationForJob(db_job) - annotation.validate_data_from_client(data) + annotation.force_set_client_id(data['create']) + client_ids = annotation.validate_data_from_client(data) annotation.delete_from_db(data['delete']) annotation.save_to_db(data['create']) @@ -90,6 +91,10 @@ def save_job(jid, data): db_job.segment.task.updated_date = timezone.now() db_job.segment.task.save() + + db_job.max_shape_id = max(db_job.max_shape_id, max(client_ids['create']) if client_ids['create'] else -1) + db_job.save() + slogger.job[jid].info("Leave save_job API: jid = {}".format(jid)) @silk_profile(name="Clear job") @@ -404,33 +409,8 @@ class _Annotation: return non_empty - def get_max_client_id(self): - max_client_id = -1 - - def extract_client_id(shape): - return shape.client_id - - if self.boxes: - max_client_id = max(max_client_id, (max(self.boxes, key=extract_client_id)).client_id) - if self.box_paths: - max_client_id = max(max_client_id, (max(self.box_paths, key=extract_client_id)).client_id) - if self.polygons: - max_client_id = max(max_client_id, (max(self.polygons, key=extract_client_id)).client_id) - if self.polygon_paths: - max_client_id = max(max_client_id, (max(self.polygon_paths, key=extract_client_id)).client_id) - if self.polylines: - max_client_id = max(max_client_id, (max(self.polylines, key=extract_client_id)).client_id) - if self.polyline_paths: - max_client_id = max(max_client_id, (max(self.polyline_paths, key=extract_client_id)).client_id) - if self.points: - max_client_id = max(max_client_id, (max(self.points, key=extract_client_id)).client_id) - if self.points_paths: - max_client_id = max(max_client_id, (max(self.points_paths, key=extract_client_id)).client_id) - - return max_client_id - # Functions below used by dump functionality - def to_boxes(self, start_client_id): + def to_boxes(self): boxes = [] for path in self.box_paths: for box in path.get_interpolated_boxes(): @@ -442,15 +422,13 @@ class _Annotation: group_id=path.group_id, occluded=box.occluded, z_order=box.z_order, - client_id=start_client_id, attributes=box.attributes + path.attributes, ) boxes.append(box) - start_client_id += 1 - return self.boxes + boxes, start_client_id + return self.boxes + boxes - def _to_poly_shapes(self, iter_attr_name, start_client_id): + def _to_poly_shapes(self, iter_attr_name): shapes = [] for path in getattr(self, iter_attr_name): for shape in path.get_interpolated_shapes(): @@ -462,24 +440,22 @@ class _Annotation: group_id=path.group_id, occluded=shape.occluded, z_order=shape.z_order, - client_id=start_client_id, attributes=shape.attributes + path.attributes, ) shapes.append(shape) - start_client_id += 1 - return shapes, start_client_id + return shapes - 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_polygons(self): + polygons = self._to_poly_shapes('polygon_paths') + return polygons + self.polygons - 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_polylines(self): + polylines = self._to_poly_shapes('polyline_paths') + return polylines + self.polylines - 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_points(self): + points = self._to_poly_shapes('points_paths') + return points + self.points def to_box_paths(self): paths = [] @@ -496,7 +472,6 @@ class _Annotation: group_id=box.group_id, boxes=[box0, box1], attributes=box.attributes, - client_id=box.client_id, ) paths.append(path) @@ -516,7 +491,6 @@ class _Annotation: stop_frame=shape.frame + 1, group_id=shape.group_id, shapes=[shape0, shape1], - client_id=shape.client_id, attributes=shape.attributes, ) paths.append(path) @@ -1353,7 +1327,7 @@ class _AnnotationForJob(_Annotation): "polylines": [], "polyline_paths": [], "points": [], - "points_paths": [] + "points_paths": [], } for box in self.boxes: @@ -1429,8 +1403,8 @@ class _AnnotationForJob(_Annotation): return data def validate_data_from_client(self, data): - db_client_ids = self._get_client_ids_from_db() client_ids = { + 'saved': self._get_client_ids_from_db(), 'create': set(), 'update': set(), 'delete': set(), @@ -1461,18 +1435,43 @@ class _AnnotationForJob(_Annotation): if tmp_res: raise Exception('More than one action for shape(s) with id={}'.format(tmp_res)) - tmp_res = (db_client_ids - client_ids['delete']) & client_ids['create'] + tmp_res = (client_ids['saved'] - client_ids['delete']) & client_ids['create'] if tmp_res: raise Exception('Trying to create new shape(s) with existing client id {}'.format(tmp_res)) - tmp_res = client_ids['delete'] - db_client_ids + tmp_res = client_ids['delete'] - client_ids['saved'] if tmp_res: raise Exception('Trying to delete shape(s) with nonexistent client id {}'.format(tmp_res)) - tmp_res = client_ids['update'] - (db_client_ids - client_ids['delete']) + tmp_res = client_ids['update'] - (client_ids['saved'] - client_ids['delete']) if tmp_res: raise Exception('Trying to update shape(s) with nonexistent client id {}'.format(tmp_res)) + max_id = self.db_job.max_shape_id + if any(new_client_id <= max_id for new_client_id in client_ids['create']): + raise Exception('Trying to create shape(s) with client id {} less than allowed value {}'.format(client_ids['create'], max_id)) + + return client_ids + + def force_set_client_id(self, data): + shape_types = ['boxes', 'points', 'polygons', 'polylines', 'box_paths', + 'points_paths', 'polygon_paths', 'polyline_paths'] + + max_id = self.db_job.max_shape_id + for shape_type in shape_types: + if not data[shape_type]: + continue + for shape in data[shape_type]: + if 'id' in shape: + max_id = max(max_id, shape['id']) + + max_id += 1 + for shape_type in shape_types: + for shape in data[shape_type]: + if 'id' not in shape or shape['id'] == -1: + shape['id'] = max_id + max_id += 1 + class _AnnotationForSegment(_Annotation): def __init__(self, db_segment): super().__init__(db_segment.start_frame, db_segment.stop_frame) @@ -1962,25 +1961,25 @@ class _AnnotationForTask(_Annotation): shapes["polygons"] = {} shapes["polylines"] = {} shapes["points"] = {} - boxes, max_client_id = self.to_boxes(self.get_max_client_id() + 1) + boxes = self.to_boxes() for box in boxes: if box.frame not in shapes["boxes"]: shapes["boxes"][box.frame] = [] shapes["boxes"][box.frame].append(box) - polygons, max_client_id = self.to_polygons(max_client_id) + polygons = self.to_polygons() for polygon in polygons: if polygon.frame not in shapes["polygons"]: shapes["polygons"][polygon.frame] = [] shapes["polygons"][polygon.frame].append(polygon) - polylines, max_client_id = self.to_polylines(max_client_id) + polylines = self.to_polylines() for polyline in polylines: if polyline.frame not in shapes["polylines"]: shapes["polylines"][polyline.frame] = [] shapes["polylines"][polyline.frame].append(polyline) - points, max_client_id = self.to_points(max_client_id) + points = self.to_points() for points in points: if points.frame not in shapes["points"]: shapes["points"][points.frame] = [] @@ -2022,7 +2021,6 @@ class _AnnotationForTask(_Annotation): ("xbr", "{:.2f}".format(shape.xbr)), ("ybr", "{:.2f}".format(shape.ybr)), ("occluded", str(int(shape.occluded))), - ("id", str(shape.client_id)), ]) if db_task.z_order: dump_dict['z_order'] = str(shape.z_order) @@ -2042,7 +2040,6 @@ class _AnnotationForTask(_Annotation): )) for p in shape.points.split(' ')) )), ("occluded", str(int(shape.occluded))), - ("id", str(shape.client_id)), ]) if db_task.z_order: @@ -2087,7 +2084,6 @@ class _AnnotationForTask(_Annotation): path_list = paths[shape_type] for path in path_list: dump_dict = OrderedDict([ - ("id", str(path.client_id)), ("label", path.label.name), ]) if path.group_id: diff --git a/cvat/apps/engine/migrations/0014_job_max_shape_id.py b/cvat/apps/engine/migrations/0014_job_max_shape_id.py new file mode 100644 index 00000000..bf3421a4 --- /dev/null +++ b/cvat/apps/engine/migrations/0014_job_max_shape_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.3 on 2018-11-23 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0013_auth_no_default_permissions'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='max_shape_id', + field=models.BigIntegerField(default=-1), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index ebc81338..1d2729d4 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -97,6 +97,7 @@ class Job(models.Model): segment = models.ForeignKey(Segment, on_delete=models.CASCADE) assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) + max_shape_id = models.BigIntegerField(default=-1) class Meta: default_permissions = () diff --git a/cvat/apps/engine/static/engine/js/annotationParser.js b/cvat/apps/engine/static/engine/js/annotationParser.js index c7d579cf..e34a780d 100644 --- a/cvat/apps/engine/static/engine/js/annotationParser.js +++ b/cvat/apps/engine/static/engine/js/annotationParser.js @@ -8,14 +8,14 @@ "use strict"; class AnnotationParser { - constructor(job, labelsInfo) { + constructor(job, labelsInfo, idGenerator) { this._parser = new DOMParser(); this._startFrame = job.start; this._stopFrame = job.stop; this._flipped = job.flipped; this._im_meta = job.image_meta_data; this._labelsInfo = labelsInfo; - this._id_set = new Set(); + this._idGen = idGenerator; } _xmlParseError(parsedXML) { @@ -133,7 +133,6 @@ class AnnotationParser { for (let track of tracks) { let label = track.getAttribute('label'); let group_id = track.getAttribute('group_id') || '0'; - let id = track.getAttribute('id') || '-1'; let labelId = this._labelsInfo.labelIdOf(label); if (labelId === null) { throw Error(`An unknown label found in the annotation file: ${label}`); @@ -151,7 +150,6 @@ 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('id', id); result.push(shapes[0]); } } @@ -212,15 +210,6 @@ class AnnotationParser { throw Error('An unknown label found in the annotation file: ' + shape.getAttribute('label')); } - let id = parseInt(shape.getAttribute('id') || '-1'); - if (id !== -1) { - if (this._id_set.has(id)) { - throw Error('More than one shape has the same id attribute'); - } - - this._id_set.add(id); - } - let attributeList = this._getAttributeList(shape, labelId); if (shape_type === 'boxes') { @@ -236,7 +225,7 @@ class AnnotationParser { ybr: ybr, z_order: z_order, attributes: attributeList, - id: id, + id: this._idGen.next(), }); } else { @@ -249,7 +238,7 @@ class AnnotationParser { occluded: occluded, z_order: z_order, attributes: attributeList, - id: id, + id: this._idGen.next(), }); } } @@ -270,7 +259,6 @@ class AnnotationParser { for (let track of tracks) { let labelId = this._labelsInfo.labelIdOf(track.getAttribute('label')); let groupId = track.getAttribute('group_id') || '0'; - let id = parseInt(track.getAttribute('id') || '-1'); if (labelId === null) { throw Error('An unknown label found in the annotation file: ' + name); } @@ -323,17 +311,9 @@ class AnnotationParser { frame: +parsed[type][0].getAttribute('frame'), attributes: [], shapes: [], - id: id, + id: this._idGen.next(), }; - if (id !== -1) { - if (this._id_set.has(id)) { - throw Error('More than one shape has the same id attribute'); - } - - this._id_set.add(id); - } - for (let shape of parsed[type]) { let keyFrame = +shape.getAttribute('keyframe'); let outside = +shape.getAttribute('outside'); @@ -405,12 +385,7 @@ class AnnotationParser { return data; } - _reset() { - this._id_set.clear(); - } - parse(text) { - this._reset(); let xml = this._parser.parseFromString(text, 'text/xml'); let parseerror = this._xmlParseError(xml); if (parseerror.length) { diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 041bbd00..53a0edca 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -100,12 +100,18 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) { window.cvat.config = new Config(); // Setup components - let annotationParser = new AnnotationParser(job, window.cvat.labelsInfo); + let idGenerator = new IncrementIdGenerator(job.max_shape_id + 1); + let annotationParser = new AnnotationParser(job, window.cvat.labelsInfo, idGenerator); - let shapeCollectionModel = new ShapeCollectionModel().import(shapeData, true); + let shapeCollectionModel = new ShapeCollectionModel(idGenerator).import(shapeData, true); let shapeCollectionController = new ShapeCollectionController(shapeCollectionModel); let shapeCollectionView = new ShapeCollectionView(shapeCollectionModel, shapeCollectionController); + // In case of old tasks that dont provide max saved shape id properly + if (job.max_shape_id === -1) { + idGenerator.reset(shapeCollectionModel.maxId + 1); + } + window.cvat.data = { get: () => shapeCollectionModel.exportAll(), set: (data) => { diff --git a/cvat/apps/engine/static/engine/js/base.js b/cvat/apps/engine/static/engine/js/base.js index 044a6fa2..14b66331 100644 --- a/cvat/apps/engine/static/engine/js/base.js +++ b/cvat/apps/engine/static/engine/js/base.js @@ -4,8 +4,16 @@ * SPDX-License-Identifier: MIT */ -/* exported confirm showMessage showOverlay dumpAnnotationRequest ExportType - createExportContainer getExportTargetContainer +/* exported + ExportType + IncrementIdGenerator + ConstIdGenerator + confirm + createExportContainer + dumpAnnotationRequest + getExportTargetContainer + showMessage + showOverlay */ "use strict"; @@ -237,6 +245,30 @@ function getExportTargetContainer(export_type, shape_type, container) { return shape_container_target; } +class IncrementIdGenerator { + constructor(startId=0) { + this._startId = startId; + } + + next() { + return this._startId++; + } + + reset(startId=0) { + this._startId = startId; + } +} + +class ConstIdGenerator { + constructor(startId=-1) { + this._startId = startId; + } + + next() { + return this._startId; + } +} + /* These HTTP methods do not require CSRF protection */ function csrfSafeMethod(method) { return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index fbd5c3d8..630b79b4 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -8,7 +8,7 @@ "use strict"; class ShapeCollectionModel extends Listener { - constructor() { + constructor(idGenereator) { super('onCollectionUpdate', () => this); this._annotationShapes = {}; this._groups = {}; @@ -54,10 +54,7 @@ class ShapeCollectionModel extends Listener { this._initialShapes = {}; this._exportedShapes = {}; this._shapesToDelete = createExportContainer(); - } - - _nextIdx() { - return this._idx++; + this._idGen = idGenereator; } _nextGroupIdx() { @@ -251,21 +248,10 @@ class ShapeCollectionModel extends Listener { } } - this._updateClientIds(); - this.notify(); return this; } - _updateClientIds() { - this._idx = Math.max(-1, ...(this._shapes.map( shape => shape.id ))) + 1; - for (const shape of this._shapes) { - if (shape.id === -1) { - shape._id = this._nextIdx(); - } - } - } - confirmExportedState() { this._initialShapes = this._exportedShapes; this._shapesToDelete = createExportContainer(); @@ -412,12 +398,11 @@ class ShapeCollectionModel extends Listener { add(data, type) { let id = null; - if (!('id' in data)) { - id = this._nextIdx(); + if (!('id' in data) || data.id === -1) { + id = this._idGen.next(); } else { id = data.id; - this._idx = Math.max(this._idx, id) + 1; } let model = buildShapeModel(data, type, id, this.nextColor()); @@ -880,6 +865,10 @@ class ShapeCollectionModel extends Listener { get shapes() { return this._shapes; } + + get maxId() { + return Math.max(-1, ...this._shapes.map( shape => shape.id )); + } } class ShapeCollectionController { diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 7f6102f6..7a735ae9 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -163,7 +163,13 @@ def get(tid): attributes[db_label.id][db_attrspec.id] = db_attrspec.text db_segments = list(db_task.segment_set.prefetch_related('job_set').all()) segment_length = max(db_segments[0].stop_frame - db_segments[0].start_frame + 1, 1) - job_indexes = [segment.job_set.first().id for segment in db_segments] + job_indexes = [] + for segment in db_segments: + db_job = segment.job_set.first() + job_indexes.append({ + "job_id": db_job.id, + "max_shape_id": db_job.max_shape_id, + }) response = { "status": db_task.status, @@ -242,7 +248,8 @@ def get_job(jid): "attributes": attributes, "z_order": db_task.z_order, "flipped": db_task.flipped, - "image_meta_data": im_meta_data + "image_meta_data": im_meta_data, + "max_shape_id": db_job.max_shape_id, } else: raise Exception("Cannot find the job: {}".format(jid)) diff --git a/cvat/apps/tf_annotation/views.py b/cvat/apps/tf_annotation/views.py index 8ec7b7eb..dbad68a7 100644 --- a/cvat/apps/tf_annotation/views.py +++ b/cvat/apps/tf_annotation/views.py @@ -204,7 +204,6 @@ def convert_to_cvat_format(data): 'delete': create_anno_container(), } - client_idx = 0 for label in data: boxes = data[label] for box in boxes: @@ -219,11 +218,9 @@ def convert_to_cvat_format(data): "group_id": 0, "occluded": False, "attributes": [], - "id": client_idx, + "id": -1, }) - client_idx += 1 - return result def create_thread(tid, labels_mapping): @@ -235,9 +232,6 @@ def create_thread(tid, labels_mapping): job.save_meta() # Get job indexes and segment length db_task = TaskModel.objects.get(pk=tid) - db_segments = list(db_task.segment_set.prefetch_related('job_set').all()) - segment_length = max(db_segments[0].stop_frame - db_segments[0].start_frame + 1, 1) - job_indexes = [segment.job_set.first().id for segment in db_segments] # Get image list image_list = make_image_list(db_task.get_data_dirname()) @@ -256,6 +250,7 @@ def create_thread(tid, labels_mapping): # Modify data format and save result = convert_to_cvat_format(result) + annotation.clear_task(tid) annotation.save_task(tid, result) slogger.glob.info('tf annotation for task {} done'.format(tid)) except: