diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index c2a13b1e292a..862ef2913c03 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -165,7 +165,7 @@ class dotdict(OrderedDict): for key in keys_for_merge: item = dotdict({v.split('__', 1)[-1]:row[v] for v in keys_for_merge[key]}) - if item.id: + if item.id is not None: merged_rows[row_id][key].append(item) # Remove redundant keys from final objects @@ -556,6 +556,14 @@ def _init_tracks_from_db(self): ] }, 'id') + # A result table can consist many equal rows for track/shape attributes + # We need filter unique attributes manually + db_track["labeledtrackattributeval_set"] = list(set(db_track["labeledtrackattributeval_set"])) + for db_shape in db_track["trackedshape_set"]: + db_shape["trackedshapeattributeval_set"] = list( + set(db_shape["trackedshapeattributeval_set"]) + ) + serializer = serializers.LabeledTrackSerializer(db_tracks, many=True) self.data["tracks"] = serializer.data @@ -924,8 +932,9 @@ def to_tracks(self): shape0 = copy.copy(shape) shape0["keyframe"] = True shape0["outside"] = False + # TODO: Separate attributes on mutable and unmutable + shape0["attributes"] = [] shape0.pop("group", None) - shape0.pop("attributes") shape1 = copy.copy(shape0) shape1["outside"] = True shape1["frame"] += 1 @@ -1099,15 +1108,17 @@ def interpolate(shape0, shape1): curr_frame = track["shapes"][0]["frame"] prev_shape = {} for shape in track["shapes"]: - if shape["frame"] != curr_frame: + if prev_shape: assert shape["frame"] > curr_frame + for attr in prev_shape["attributes"]: + if attr["spec_id"] not in map(lambda el: el["spec_id"], shape["attributes"]): + shape["attributes"].append(copy.deepcopy(attr)) if not prev_shape["outside"]: shapes.extend(interpolate(prev_shape, shape)) - if not shape["outside"]: - shape["keyframe"] = True - shapes.append(shape) - curr_frame = shape["frame"] + 1 + shape["keyframe"] = True + shapes.append(shape) + curr_frame = shape["frame"] prev_shape = shape # TODO: Need to modify a client and a database (append "outside" shapes for polytracks) diff --git a/cvat/apps/engine/static/engine/js/annotationSaver.js b/cvat/apps/engine/static/engine/js/annotationSaver.js index 40759d8d5ab3..b3ce668dc3fb 100644 --- a/cvat/apps/engine/static/engine/js/annotationSaver.js +++ b/cvat/apps/engine/static/engine/js/annotationSaver.js @@ -24,11 +24,14 @@ class AnnotationSaverModel extends Listener { this._hash = this._getHash(); - for (const shape of initialData.shapes) { + // We need use data from export instead of initialData + // Otherwise we have differ keys order and JSON comparison code incorrect + const data = this._shapeCollection.export()[0]; + for (const shape of data.shapes) { this._initialObjects[shape.id] = shape; } - for (const track of initialData.tracks) { + for (const track of data.tracks) { this._initialObjects[track.id] = track; } } @@ -177,6 +180,7 @@ class AnnotationSaverModel extends Listener { } _updateCreatedObjects(objectsToSave, savedObjects, mapping) { + // Method setups IDs of created objects after saving on a server const allSavedObjects = savedObjects.shapes.concat(savedObjects.tracks); const allObjectsToSave = objectsToSave.shapes.concat(objectsToSave.tracks); if (allSavedObjects.length !== allObjectsToSave.length) { @@ -185,7 +189,9 @@ class AnnotationSaverModel extends Listener { for (let idx = 0; idx < allSavedObjects.length; idx += 1) { const objectModel = mapping.filter(el => el[0] === allObjectsToSave[idx])[0][1]; - objectModel.serverID = allSavedObjects[idx].id; + const { id } = allSavedObjects[idx]; + objectModel.serverID = id; + allObjectsToSave[idx].id = id; } this._shapeCollection.update(); @@ -218,6 +224,7 @@ class AnnotationSaverModel extends Listener { const savedObjects = await this._put(data); this._updateCreatedObjects(exported, savedObjects, mapping); this._shapeCollection.flush = false; + this._version = savedObjects.version; for (const object of savedObjects.shapes.concat(savedObjects.tracks)) { this._initialObjects[object.id] = object; } @@ -228,13 +235,15 @@ class AnnotationSaverModel extends Listener { this.notify('saveCreated'); const savedCreated = await this._create(created); this._updateCreatedObjects(created, savedCreated, mapping); - for (const object of savedCreated.shapes.concat(savedCreated.tracks)) { + this._version = savedCreated.version; + for (const object of created.shapes.concat(created.tracks)) { this._initialObjects[object.id] = object; } this.notify('saveUpdated'); const savedUpdated = await this._update(updated); - for (const object of savedUpdated.shapes.concat(savedUpdated.tracks)) { + this._version = savedUpdated.version; + for (const object of updated.shapes.concat(updated.tracks)) { if (object.id in this._initialObjects) { this._initialObjects[object.id] = object; } @@ -242,6 +251,7 @@ class AnnotationSaverModel extends Listener { this.notify('saveDeleted'); const savedDeleted = await this._delete(deleted); + this._version = savedDeleted.version; for (const object of savedDeleted.shapes.concat(savedDeleted.tracks)) { if (object.id in this._initialObjects) { delete this._initialObjects[object.id];