diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 234c2267991..f6c283767a6 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -761,14 +761,29 @@ def init_from_client(self, data): label = _Label(self.db_labels[int(path['label_id'])]) boxes = [] frame = -1 + + has_boxes_on_prev_segm = False + last_box_on_prev_segm = None + has_box_on_start_frame = False + for box in path['shapes']: + if int(box['frame']) < self.start_frame: + has_boxes_on_prev_segm = True + if last_box_on_prev_segm is None or int(last_box_on_prev_segm["frame"]) < int(box["frame"]): + last_box_on_prev_segm = box + elif int(box['frame']) == self.start_frame: + has_box_on_start_frame = True + break + if has_boxes_on_prev_segm and not has_box_on_start_frame: + last_box_on_prev_segm["frame"] = self.start_frame + for box in path['shapes']: - if int(box['frame']) <= self.stop_frame: + if int(box['frame']) <= self.stop_frame and int(box['frame']) >= self.start_frame: frame_idx = int(box['frame']) if db_task.mode == 'annotation' else 0 xtl, ytl, xbr, ybr = self._clamp_box(float(box['xtl']), float(box['ytl']), float(box['xbr']), float(box['ybr']), image_meta['original_size'][frame_idx]) tracked_box = _TrackedBox(xtl, ytl, xbr, ybr, int(box['frame']), strtobool(str(box['occluded'])), int(box['z_order']), strtobool(str(box['outside']))) - assert tracked_box.frame > frame + assert tracked_box.frame > frame frame = tracked_box.frame for attr in box['attributes']: @@ -780,7 +795,7 @@ def init_from_client(self, data): boxes.append(tracked_box) else: self.logger.error("init_from_client: ignore frame #%d " + - "because stop_frame is %d", int(box['frame']), self.stop_frame) + "because it out of segment range [%d-%d]", int(box['frame']), self.start_frame, self.stop_frame) attributes = [] for attr in path['attributes']: @@ -790,7 +805,7 @@ def init_from_client(self, data): attributes.append(attr) assert frame <= self.stop_frame - box_path = _BoxPath(label, int(path['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) self.box_paths.append(box_path) @@ -799,8 +814,23 @@ def init_from_client(self, data): label = _Label(self.db_labels[int(path['label_id'])]) poly_shapes = [] frame = -1 + + has_shapes_on_prev_segm = False + last_shape_on_prev_segm = None + has_shape_on_start_frame = False + for poly_shape in path['shapes']: + if int(poly_shape['frame']) < self.start_frame: + has_shapes_on_prev_segm = True + if last_shape_on_prev_segm is None or int(last_shape_on_prev_segm["frame"]) < (poly_shape["frame"]): + last_shape_on_prev_segm = box + elif int(poly_shape['frame']) == self.start_frame: + has_shape_on_start_frame = True + break + if has_shapes_on_prev_segm and not has_shape_on_start_frame: + last_shape_on_prev_segm["frame"] = self.start_frame + for poly_shape in path['shapes']: - if int(poly_shape['frame']) <= self.stop_frame: + 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'])), @@ -817,7 +847,7 @@ def init_from_client(self, data): poly_shapes.append(tracked_poly_shape) else: self.logger.error("init_from_client: ignore frame #%d " + - "because stop_frame is %d", int(poly_shape['frame']), self.stop_frame) + "because it out of segment range [%d-%d]", int(poly_shape['frame']), self.start_frame, self.stop_frame) attributes = [] for attr in path['attributes']: @@ -826,7 +856,7 @@ def init_from_client(self, data): attr = _Attribute(spec, str(attr['value'])) attributes.append(attr) - poly_path = _PolyPath(label, int(path['frame']), self.stop_frame + 1, + poly_path = _PolyPath(label, min(list(map(lambda shape: shape.frame, poly_shapes))), self.stop_frame + 1, int(path['group_id']), poly_shapes, attributes) getattr(self, poly_path_type).append(poly_path) diff --git a/cvat/apps/engine/migrations/0008_auto_20180917_1424.py b/cvat/apps/engine/migrations/0008_auto_20180917_1424.py new file mode 100644 index 00000000000..cf6b45500d9 --- /dev/null +++ b/cvat/apps/engine/migrations/0008_auto_20180917_1424.py @@ -0,0 +1,20 @@ +# Generated by Django 2.0.3 on 2018-09-17 11:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0007_task_flipped'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='owner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/cvat/apps/engine/migrations/0009_auto_20180917_1424.py b/cvat/apps/engine/migrations/0009_auto_20180917_1424.py new file mode 100644 index 00000000000..a20ec2cf95f --- /dev/null +++ b/cvat/apps/engine/migrations/0009_auto_20180917_1424.py @@ -0,0 +1,170 @@ +# Generated by Django 2.0.3 on 2018-09-17 11:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0008_auto_20180917_1424'), + ] + + + + operations = [ + migrations.AlterField( + model_name='labeledbox', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledboxattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledpoints', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledpointsattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledpolygon', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledpolygonattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledpolyline', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='labeledpolylineattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='objectpath', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='objectpathattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedbox', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedboxattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedpoints', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedpointsattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedpolygon', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedpolygonattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedpolyline', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='trackedpolylineattributeval', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='objectpathattributeval', + name='track_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='objectpathattributeval', + name='track_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedpoints', + name='track_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedpolygon', + name='track_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedpolyline', + name='track_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedboxattributeval', + name='box_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedpointsattributeval', + name='points_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedpolygonattributeval', + name='polygon_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='trackedpolylineattributeval', + name='polyline_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='labeledboxattributeval', + name='box_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='labeledpointsattributeval', + name='points_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='labeledpolygonattributeval', + name='polygon_id', + field=models.BigIntegerField(), + ), + migrations.AlterField( + model_name='labeledpolylineattributeval', + name='polyline_id', + field=models.BigIntegerField(), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index a77efd3e431..6883018b194 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -128,6 +128,7 @@ def __str__(self): class AttributeVal(models.Model): # TODO: add a validator here to be sure that it corresponds to self.label + id = models.BigAutoField(primary_key=True) spec = models.ForeignKey(AttributeSpec, on_delete=models.CASCADE) value = models.CharField(max_length=64) class Meta: @@ -148,6 +149,7 @@ class Meta: abstract = True class BoundingBox(Shape): + id = models.BigAutoField(primary_key=True) xtl = models.FloatField() ytl = models.FloatField() xbr = models.FloatField() @@ -156,6 +158,7 @@ class Meta: abstract = True class PolyShape(Shape): + id = models.BigAutoField(primary_key=True) points = models.TextField() class Meta: abstract = True @@ -185,6 +188,7 @@ class LabeledPointsAttributeVal(AttributeVal): points = models.ForeignKey(LabeledPoints, on_delete=models.CASCADE) class ObjectPath(Annotation): + id = models.BigAutoField(primary_key=True) shapes = models.CharField(max_length=10, default='boxes') class ObjectPathAttributeVal(AttributeVal): diff --git a/cvat/apps/engine/static/engine/base.css b/cvat/apps/engine/static/engine/base.css index ce19a39bcf2..30b2b12873b 100644 --- a/cvat/apps/engine/static/engine/base.css +++ b/cvat/apps/engine/static/engine/base.css @@ -98,8 +98,9 @@ html { border: none; outline: none; cursor: pointer; - padding: 14px 16px; + padding: 3px 0px; transition: 0.3s; + } .tab button:hover { diff --git a/cvat/apps/engine/static/engine/js/3rdparty.patch b/cvat/apps/engine/static/engine/js/3rdparty.patch index f3a216368a7..38f26000b63 100644 --- a/cvat/apps/engine/static/engine/js/3rdparty.patch +++ b/cvat/apps/engine/static/engine/js/3rdparty.patch @@ -1,29 +1,29 @@ -From b89380c65ea8bc9231cc98a6ae0e812227c85b3d Mon Sep 17 00:00:00 2001 +From 5eeb1092c64865c555671ed585da18f974c9c10c Mon Sep 17 00:00:00 2001 From: Boris Sekachev -Date: Tue, 10 Jul 2018 14:31:13 +0300 +Date: Tue, 18 Sep 2018 15:58:20 +0300 Subject: [PATCH] tmp --- .../engine/static/engine/js/3rdparty/svg.draggable.js | 1 + cvat/apps/engine/static/engine/js/3rdparty/svg.draw.js | 17 +++++++++++++++-- .../apps/engine/static/engine/js/3rdparty/svg.resize.js | 5 +++-- - .../apps/engine/static/engine/js/3rdparty/svg.select.js | 1 + - 4 files changed, 20 insertions(+), 4 deletions(-) + .../apps/engine/static/engine/js/3rdparty/svg.select.js | 5 ++++- + 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/cvat/apps/engine/static/engine/js/3rdparty/svg.draggable.js b/cvat/apps/engine/static/engine/js/3rdparty/svg.draggable.js -index d88abf5..06158f1 100644 +index d88abf5..aba474c 100644 --- a/cvat/apps/engine/static/engine/js/3rdparty/svg.draggable.js +++ b/cvat/apps/engine/static/engine/js/3rdparty/svg.draggable.js @@ -109,6 +109,7 @@ - + // while dragging DragHandler.prototype.drag = function(e){ + this.m = this.el.node.getScreenCTM().inverse(); - + var box = this.getBBox() , p = this.transformPoint(e) diff --git a/cvat/apps/engine/static/engine/js/3rdparty/svg.draw.js b/cvat/apps/engine/static/engine/js/3rdparty/svg.draw.js -index 68dbf2a..9884b75 100644 +index 68dbf2a..20a6917 100644 --- a/cvat/apps/engine/static/engine/js/3rdparty/svg.draw.js +++ b/cvat/apps/engine/static/engine/js/3rdparty/svg.draw.js @@ -18,6 +18,7 @@ @@ -31,13 +31,13 @@ index 68dbf2a..9884b75 100644 this.lastUpdateCall = null; this.options = {}; + this.set = new SVG.Set(); - + // Merge options and defaults for (var i in this.el.draw.defaults) { @@ -139,6 +140,8 @@ // Call the calc-function which calculates the new position and size this.calc(event); - + + this.m = this.el.node.getScreenCTM().inverse(); + this.offset = { x: window.pageXOffset, y: window.pageYOffset }; // Fire the `drawupdate`-event @@ -46,7 +46,7 @@ index 68dbf2a..9884b75 100644 @@ -160,6 +163,16 @@ this.el.fire('drawcancel'); }; - + + // Undo last drawed point + PaintHandler.prototype.undo = function () { + if (this.set.length()) { @@ -59,24 +59,24 @@ index 68dbf2a..9884b75 100644 + // Calculate the corrected position when using `snapToGrid` PaintHandler.prototype.snapToGrid = function (draw) { - + @@ -371,14 +384,14 @@ - + this.set.clear(); - + - for (var i = 0; i < array.length; ++i) { + for (var i = 0; i < array.length - 1; ++i) { - + this.p.x = array[i][0] this.p.y = array[i][1] - + var p = this.p.matrixTransform(this.parent.node.getScreenCTM().inverse().multiply(this.el.node.getScreenCTM())); - + - this.set.add(this.parent.circle(5).stroke({width: 1}).fill('#ccc').center(p.x, p.y)); + this.set.add(this.parent.circle(5).stroke({width: 1}).fill('#ccc').center(p.x, p.y)).addClass("svg_draw_point"); } } - + diff --git a/cvat/apps/engine/static/engine/js/3rdparty/svg.resize.js b/cvat/apps/engine/static/engine/js/3rdparty/svg.resize.js index 0c3b63d..fb5dc26 100644 --- a/cvat/apps/engine/static/engine/js/3rdparty/svg.resize.js @@ -91,27 +91,53 @@ index 0c3b63d..fb5dc26 100644 + y: event.clientY != null ? event.clientY : event.touches[0].clientY }; }; - + @@ -343,6 +343,7 @@ } return; } + this.m = this.el.node.getScreenCTM().inverse(); - + // Calculate the difference between the mouseposition at start and now var txPt = this._extractPosition(event); diff --git a/cvat/apps/engine/static/engine/js/3rdparty/svg.select.js b/cvat/apps/engine/static/engine/js/3rdparty/svg.select.js -index 47e07bd..f1d0c02 100644 +index 47e07bd..cee6d34 100644 --- a/cvat/apps/engine/static/engine/js/3rdparty/svg.select.js +++ b/cvat/apps/engine/static/engine/js/3rdparty/svg.select.js @@ -160,6 +160,7 @@ SelectHandler.prototype.drawPoints = function () { ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; ev.stopPropagation(); - + + if (ev.which != 1) return false; var x = ev.pageX || ev.touches[0].pageX; var y = ev.pageY || ev.touches[0].pageY; _this.el.fire('point', {x: x, y: y, i: k, event: ev}); --- +@@ -361,6 +362,7 @@ SelectHandler.prototype.cleanup = function () { + // stop watching the element, remove the selection + this.rectSelection.set.each(function () { + this.remove(); ++ SVG.off(this.node); + }); + + this.rectSelection.set.clear(); +@@ -371,6 +373,7 @@ SelectHandler.prototype.cleanup = function () { + // Remove all points, clear the set, stop watching the element + this.pointSelection.set.each(function () { + this.remove(); ++ SVG.off(this.node); + }); + + this.pointSelection.set.clear(); +@@ -379,8 +382,8 @@ SelectHandler.prototype.cleanup = function () { + + if (!this.pointSelection.isSelected && !this.rectSelection.isSelected) { + this.nested.remove(); ++ SVG.off(this.node); + delete this.nested; +- + } + }; + +-- 2.7.4 diff --git a/cvat/apps/engine/static/engine/js/annotationParser.js b/cvat/apps/engine/static/engine/js/annotationParser.js index 0f9ee9a774c..92536516ec3 100644 --- a/cvat/apps/engine/static/engine/js/annotationParser.js +++ b/cvat/apps/engine/static/engine/js/annotationParser.js @@ -322,8 +322,6 @@ class AnnotationParser { Ignore all frames more then stop. */ let significant = keyFrame || frame === this._startFrame; - significant = significant && frame >= this._startFrame; - significant = significant && frame <= this._stopFrame; if (significant) { let attributeList = this._getAttributeList(shape, labelId); @@ -348,7 +346,7 @@ class AnnotationParser { path.attributes = pathAttributes; if (type === 'boxes') { - let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, frame); + let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, Math.clamp(frame, this._startFrame, this._stopFrame)); path.shapes.push({ frame: frame, occluded: occluded, @@ -362,7 +360,7 @@ class AnnotationParser { }); } else { - let [points, occluded, z_order] = this._getPolyPosition(shape, frame); + let [points, occluded, z_order] = this._getPolyPosition(shape, Math.clamp(frame, this._startFrame, this._stopFrame)); path.shapes.push({ frame: frame, occluded: occluded, diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index e24c2a44c44..3bd09d350c6 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -12,8 +12,10 @@ function callAnnotationUI(jid) { let loadJobEvent = Logger.addContinuedEvent(Logger.EventType.loadJob); serverRequest("/get/job/" + jid, function(job) { serverRequest("get/annotation/job/" + jid, function(data) { - buildAnnotationUI(job, data, loadJobEvent); $('#loadingOverlay').remove(); + setTimeout(() => { + buildAnnotationUI(job, data, loadJobEvent); + }, 0); }); }); } diff --git a/cvat/apps/engine/static/engine/js/player.js b/cvat/apps/engine/static/engine/js/player.js index dcfad56c4ca..3c859f548d1 100644 --- a/cvat/apps/engine/static/engine/js/player.js +++ b/cvat/apps/engine/static/engine/js/player.js @@ -44,6 +44,8 @@ class FrameProvider extends Listener { this._loaded = frame; this._frameCollection[frame] = image; this._loadAllowed = true; + image.onload = null; + image.onerror = null; this.notify(); } @@ -109,6 +111,8 @@ class FrameProvider extends Listener { image.onload = this._onImageLoad.bind(this, image, frame); image.onerror = () => { this._loadAllowed = true; + image.onload = null; + image.onerror = null; }; image.src = `get/task/${this._tid}/frame/${frame}`; }.bind(this), 25); diff --git a/cvat/apps/engine/static/engine/js/shapeBuffer.js b/cvat/apps/engine/static/engine/js/shapeBuffer.js index 8c0efe4f4c9..42ef6bf5f67 100644 --- a/cvat/apps/engine/static/engine/js/shapeBuffer.js +++ b/cvat/apps/engine/static/engine/js/shapeBuffer.js @@ -149,7 +149,7 @@ class ShapeBufferModel extends Listener { this._collection.add(object, `annotation_${this._shape.type}`); } - // Undo/redo code + // Undo/redo code let model = this._collection.shapes.slice(-1)[0]; window.cvat.addAction('Paste Object', () => { model.removed = true; diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 1ee31a462b0..bdb08126fde 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -1247,8 +1247,17 @@ class ShapeCollectionView { view.erase(); } - this._currentViews = []; + // Save parents and detach elements from DOM + // in order to increase performance in the buildShapeView function + let parents = { + uis: this._UIContent.parent(), + shapes: this._frameContent.node.parentNode + }; + + this._frameContent.node.parent = null; + this._UIContent.detach(); + this._currentViews = []; for (let shape of collection.currentShapes) { let model = shape.model; let view = buildShapeView(model, buildShapeController(model), this._frameContent, this._UIContent); @@ -1259,6 +1268,10 @@ class ShapeCollectionView { view.subscribe(this); this._labelsContent.find(`.labelContentElement[label_id="${model.label}"]`).removeClass('hidden'); } + + parents.shapes.append(this._frameContent.node); + parents.uis.prepend(this._UIContent); + ShapeCollectionView.sortByZOrder(); } diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js index a1a58149fdc..dc797fc2287 100644 --- a/cvat/apps/engine/static/engine/js/shapeCreator.js +++ b/cvat/apps/engine/static/engine/js/shapeCreator.js @@ -261,6 +261,75 @@ class ShapeCreatorView { }.bind(this)); } + + _createPolyEvents() { + // If number of points for poly shape specified, use it. + // Dicrement number on draw new point events. Drawstart trigger when create first point + + if (this._polyShapeSize) { + let size = this._polyShapeSize; + let sizeDecrement = function() { + if (!--size) { + this._drawInstance.draw('done'); + } + }.bind(this); + + let sizeIncrement = function() { + size ++; + }; + + this._drawInstance.on('drawstart', sizeDecrement); + this._drawInstance.on('drawpoint', sizeDecrement); + this._drawInstance.on('undopoint', sizeIncrement); + } + // Otherwise draw will stop by Ctrl + N press + + // Callbacks for point scale + this._drawInstance.on('drawstart', this._rescaleDrawPoints.bind(this)); + this._drawInstance.on('drawpoint', this._rescaleDrawPoints.bind(this)); + this._frameContent.on('mousedown.shapeCreator', (e) => { + if (e.which === 3) { + this._drawInstance.draw('undo'); + } + }); + + this._drawInstance.on('drawstop', () => { + this._frameContent.off('mousedown.shapeCreator'); + }); + // Also we need callback on drawdone event for get points + this._drawInstance.on('drawdone', function(e) { + let points = PolyShapeModel.convertStringToNumberArray(e.target.getAttribute('points')); + for (let point of points) { + point.x = Math.clamp(point.x, 0, window.cvat.player.geometry.frameWidth); + point.y = Math.clamp(point.y, 0, window.cvat.player.geometry.frameHeight); + } + + // Min 2 points for polyline and 3 points for polygon + if (points.length) { + if (this._type === 'polyline' && points.length < 2) { + showMessage("Min 2 points must be for polyline drawing."); + } + else if (this._type === 'polygon' && points.length < 3) { + showMessage("Min 3 points must be for polygon drawing."); + } + else { + points = PolyShapeModel.convertNumberArrayToString(points); + + // Update points in view in order to get updated box + e.target.setAttribute('points', points); + let box = e.target.getBBox(); + if (box.width * box.height >= AREA_TRESHOLD || this._type === 'points' || + this._type === 'polyline' && (box.width >= AREA_TRESHOLD || box.height >= AREA_TRESHOLD)) { + this._controller.finish({points: e.target.getAttribute('points')}, this._type); + } + } + } + + this._controller.switchCreateMode(true); + }.bind(this)); + } + + _create() { let sizeUI = null; switch(this._type) { @@ -302,7 +371,7 @@ class ShapeCreatorView { this._drawInstance = this._frameContent.polyline().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({ 'stroke-width': 0, }); - createPolyEvents.call(this); + this._createPolyEvents(); break; case 'polygon': if (this._polyShapeSize && this._polyShapeSize < 3) { @@ -315,7 +384,7 @@ class ShapeCreatorView { this._drawInstance = this._frameContent.polygon().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({ 'stroke-width': STROKE_WIDTH / this._scale, }); - createPolyEvents.call(this); + this._createPolyEvents(); break; case 'polyline': if (this._polyShapeSize && this._polyShapeSize < 2) { @@ -328,7 +397,7 @@ class ShapeCreatorView { this._drawInstance = this._frameContent.polyline().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({ 'stroke-width': STROKE_WIDTH / this._scale, }); - createPolyEvents.call(this); + this._createPolyEvents(); break; default: throw Error(`Bad type found ${this._type}`); @@ -368,73 +437,6 @@ class ShapeCreatorView { this._drawInstance.attr({ 'z_order': Number.MAX_SAFE_INTEGER, }); - - function createPolyEvents() { - // If number of points for poly shape specified, use it. - // Dicrement number on draw new point events. Drawstart trigger when create first point - - if (this._polyShapeSize) { - let size = this._polyShapeSize; - let sizeDecrement = function() { - if (!--size) { - this._drawInstance.draw('done'); - } - }.bind(this); - - let sizeIncrement = function() { - size ++; - }; - - this._drawInstance.on('drawstart', sizeDecrement); - this._drawInstance.on('drawpoint', sizeDecrement); - this._drawInstance.on('undopoint', sizeIncrement); - } - // Otherwise draw will stop by Ctrl + N press - - // Callbacks for point scale - this._drawInstance.on('drawstart', this._rescaleDrawPoints.bind(this)); - this._drawInstance.on('drawpoint', this._rescaleDrawPoints.bind(this)); - this._frameContent.on('mousedown.shapeCreator', (e) => { - if (e.which === 3) { - this._drawInstance.draw('undo'); - } - }); - - this._drawInstance.on('drawstop', () => { - this._frameContent.off('mousedown.shapeCreator'); - }); - // Also we need callback on drawdone event for get points - this._drawInstance.on('drawdone', function(e) { - let points = PolyShapeModel.convertStringToNumberArray(e.target.getAttribute('points')); - for (let point of points) { - point.x = Math.clamp(point.x, 0, window.cvat.player.geometry.frameWidth); - point.y = Math.clamp(point.y, 0, window.cvat.player.geometry.frameHeight); - } - - // Min 2 points for polyline and 3 points for polygon - if (points.length) { - if (this._type === 'polyline' && points.length < 2) { - showMessage("Min 2 points must be for polyline drawing."); - } - else if (this._type === 'polygon' && points.length < 3) { - showMessage("Min 3 points must be for polygon drawing."); - } - else { - points = PolyShapeModel.convertNumberArrayToString(points); - - // Update points in view in order to get updated box - e.target.setAttribute('points', points); - let box = e.target.getBBox(); - if (box.width * box.height >= AREA_TRESHOLD || this._type === 'points' || - this._type === 'polyline' && (box.width >= AREA_TRESHOLD || box.height >= AREA_TRESHOLD)) { - this._controller.finish({points: e.target.getAttribute('points')}, this._type); - } - } - } - - this._controller.switchCreateMode(true); - }.bind(this)); - } } _rescaleDrawPoints() { @@ -477,7 +479,7 @@ class ShapeCreatorView { this._type = model.defaultType; this._mode = model.defaultMode; - if (this._type === 'box') { + if (!['polygon', 'polyline', 'points'].includes(this._type)) { this._drawAim(); } @@ -486,10 +488,7 @@ class ShapeCreatorView { this._create(); } else { - if (this._type === 'box') { - this._removeAim(); - } - + this._removeAim(); this._cancel = true; this._createButton.text("Create Shape"); document.oncontextmenu = null; @@ -526,7 +525,7 @@ class ShapeCreatorView { this._aim.x.attr('stroke-width', STROKE_WIDTH / this._scale); this._aim.y.attr('stroke-width', STROKE_WIDTH / this._scale); } - if (this._type != 'points') { + if (['box', 'polygon', 'polyline'].includes(this._type)) { this._drawInstance.attr('stroke-width', STROKE_WIDTH / this._scale); } } diff --git a/cvat/apps/engine/static/engine/js/shapeFilter.js b/cvat/apps/engine/static/engine/js/shapeFilter.js index 564d421fa7a..9781812b556 100644 --- a/cvat/apps/engine/static/engine/js/shapeFilter.js +++ b/cvat/apps/engine/static/engine/js/shapeFilter.js @@ -26,10 +26,11 @@ class FilterModel { lock: shape.model.lock }; + // We replace all dashes due to defiant.js can't work with it function convertAttributes(attributes) { let converted = {}; for (let attrId in attributes) { - converted[attributes[attrId].name.toLowerCase()] = ('' + attributes[attrId].value).toLowerCase(); + converted[attributes[attrId].name.toLowerCase().replace(/-/g, "_")] = ('' + attributes[attrId].value).toLowerCase(); } return converted; } @@ -38,11 +39,11 @@ class FilterModel { _convertCollection(collection) { let converted = {}; for (let labelId in this._labels) { - converted[this._labels[labelId]] = []; + converted[this._labels[labelId].replace(/-/g, "_")] = []; } for (let shape of collection) { - converted[this._labels[shape.model.label].toLowerCase()].push(this._convertShape(shape)); + converted[this._labels[shape.model.label].toLowerCase().replace(/-/g, "_")].push(this._convertShape(shape)); } return converted; } @@ -109,7 +110,7 @@ class FilterView { this._filterString.on('change', (e) => { let value = $.trim(e.target.value); if (value.length) { - value = value.split('|').map(x => '/d:data/' + x).join('|').toLowerCase(); + value = value.split('|').map(x => '/d:data/' + x).join('|').toLowerCase().replace(/-/g, "_"); } if (this._controller.updateFilter(value)) { this._filterString.css('color', 'green'); diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 403712457ef..a38c1bb6e47 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -22,7 +22,9 @@ class ShapeModel extends Listener { this._type = type; this._color = color; this._label = data.label_id; - this._frame = data.frame; + this._frame = type.split('_')[0] === 'annotation' ? data.frame : + positions.filter((pos) => pos.frame < window.cvat.player.frames.start).length ? + window.cvat.player.frames.start : Math.min(...positions.map((pos) => pos.frame)); this._removed = false; this._locked = false; this._merging = false; @@ -333,7 +335,17 @@ class ShapeModel extends Listener { let oldPos = Object.assign({}, this._positions[frame]); window.cvat.addAction('Change Outside', () => { if (!Object.keys(oldPos).length) { + // Frame hasn't been a keyframe, remove it from position and redestribute attributes delete this._positions[frame]; + this._frame = Math.min(...Object.keys(this._positions).map((el) => +el)); + if (frame < this._frame && frame in this._attributes.mutable) { + this._attributes.mutable[this._frame] = this._attributes.mutable[frame]; + } + + if (frame in this._attributes.mutable) { + delete this._attributes.mutable[frame]; + } + this._updateReason = 'outside'; this.notify(); } @@ -349,6 +361,15 @@ class ShapeModel extends Listener { position.outside = !position.outside; this.updatePosition(frame, position, true); + // Update the start frame if need and redestribute attributes + if (frame < this._frame) { + if (this._frame in this._attributes.mutable) { + this._attributes.mutable[frame] = this._attributes.mutable[this._frame]; + delete(this._attributes.mutable[this._frame]); + } + this._frame = frame; + } + this._updateReason = 'outside'; this.notify(); } @@ -360,38 +381,28 @@ class ShapeModel extends Listener { } // Undo/redo code - let oldPos = Object.assign({}, this._positions[frame]); window.cvat.addAction('Change Keyframe', () => { - if (!Object.keys(oldPos).length) { - delete this._positions[frame]; - this._updateReason = 'outside'; - this.notify(); - } - else { - this.updatePosition(frame, oldPos, true); - this._updateReason = 'keyframe'; - this.notify(); - } + this.switchKeyFrame(frame); }, () => { this.switchKeyFrame(frame); }, frame); // End of undo/redo code if (frame in this._positions && Object.keys(this._positions).length > 1) { - // If frame is first frame, need to redestribute attributes to new first frame + // If frame is first object frame, need redestribute attributes if (frame === this._frame) { - this._frame = +Object.keys(this._positions).sort((a,b) => +a - +b)[1]; + this._frame = Object.keys(this._positions).map((el) => +el).sort((a,b) => a - b)[1]; if (frame in this._attributes.mutable) { this._attributes.mutable[this._frame] = this._attributes.mutable[frame]; + delete(this._attributes.mutable[frame]); } } - delete(this._positions[frame]); - if (frame in this._attributes.mutable) { - delete(this._attributes.mutable[frame]); - } } else { + let position = this._interpolatePosition(frame); + this.updatePosition(frame, position, true); + if (frame < this._frame) { if (this._frame in this._attributes.mutable) { this._attributes.mutable[frame] = this._attributes.mutable[this._frame]; @@ -399,9 +410,6 @@ class ShapeModel extends Listener { } this._frame = frame; } - - let position = this._interpolatePosition(frame); - this.updatePosition(frame, position, true); } this._updateReason = 'keyframe'; this.notify(); @@ -803,17 +811,44 @@ class BoxModel extends ShapeModel { static importPositions(positions) { let imported = {}; if (this._type === 'interpolation_box') { + let last_key_in_prev_segm = null; + let segm_start = window.cvat.player.frames.start; + let segm_stop = window.cvat.player.frames.stop; + for (let pos of positions) { - imported[pos.frame] = { - xtl: pos.xtl, - ytl: pos.ytl, - xbr: pos.xbr, - ybr: pos.ybr, - occluded: pos.occluded, - outside: pos.outside, - z_order: pos.z_order, + let frame = pos.frame; + + if (frame >= segm_start && frame <= segm_stop) { + imported[frame] = { + xtl: pos.xtl, + ytl: pos.ytl, + xbr: pos.xbr, + ybr: pos.ybr, + occluded: pos.occluded, + outside: pos.outside, + z_order: pos.z_order, + }; + } + else { + console.log(`Frame ${frame} has been found in segment [${segm_start}-${segm_stop}]. It have been ignored.`); + if (!last_key_in_prev_segm || frame > last_key_in_prev_segm.frame) { + last_key_in_prev_segm = pos; + } + } + } + + if (last_key_in_prev_segm && !(segm_start in imported)) { + imported[segm_start] = { + xtl: last_key_in_prev_segm.xtl, + ytl: last_key_in_prev_segm.ytl, + xbr: last_key_in_prev_segm.xbr, + ybr: last_key_in_prev_segm.ybr, + occluded: last_key_in_prev_segm.occluded, + outside: last_key_in_prev_segm.outside, + z_order: last_key_in_prev_segm.z_order, }; } + return imported; } @@ -837,6 +872,7 @@ class PolyShapeModel extends ShapeModel { this._setupKeyFrames(); } + _interpolatePosition(frame) { if (frame in this._positions) { return Object.assign({}, this._positions[frame], { @@ -1036,14 +1072,37 @@ class PolyShapeModel extends ShapeModel { static importPositions(positions) { let imported = {}; if (this._type.startsWith('interpolation')) { + let last_key_in_prev_segm = null; + let segm_start = window.cvat.player.frames.start; + let segm_stop = window.cvat.player.frames.stop; + for (let pos of positions) { - imported[pos.frame] = { - points: pos.points, - occluded: pos.occluded, - outside: pos.outside, - z_order: pos.z_order, + let frame = pos.frame; + if (frame >= segm_start && frame <= segm_stop) { + imported[pos.frame] = { + points: pos.points, + occluded: pos.occluded, + outside: pos.outside, + z_order: pos.z_order, + }; + } + else { + console.log(`Frame ${frame} has been found in segment [${segm_start}-${segm_stop}]. It have been ignored.`); + if (!last_key_in_prev_segm || frame > last_key_in_prev_segm.frame) { + last_key_in_prev_segm = pos; + } + } + } + + if (last_key_in_prev_segm && !(segm_start in imported)) { + imported[segm_start] = { + points: last_key_in_prev_segm.points, + occluded: last_key_in_prev_segm.occluded, + outside: last_key_in_prev_segm.outside, + z_order: last_key_in_prev_segm.z_order, }; } + return imported; } @@ -1587,8 +1646,8 @@ class ShapeView extends Listener { _removeShapeUI() { if (this._uis.shape) { - this._uis.shape.off('click'); this._uis.shape.remove(); + SVG.off(this._uis.shape.node); this._uis.shape = null; } } @@ -1597,6 +1656,7 @@ class ShapeView extends Listener { _removeShapeText() { if (this._uis.text) { this._uis.text.remove(); + SVG.off(this._uis.text.node); this._uis.text = null; } } diff --git a/cvat/apps/engine/static/engine/stylesheet.css b/cvat/apps/engine/static/engine/stylesheet.css index 2a9306acce8..4e53b72d2db 100644 --- a/cvat/apps/engine/static/engine/stylesheet.css +++ b/cvat/apps/engine/static/engine/stylesheet.css @@ -356,6 +356,9 @@ .customizedTab { border-radius: 5px 5px 0px 0px; width: 100%; + width: 15%; + float: left; + margin: 0px 10px; } /* ----------------------- IDs ----------------------- */ @@ -392,7 +395,9 @@ } #uiContent, #trackManagement, #aamMenu, #labelsContent { - border: 1px black solid; + border-bottom: 1px solid black; + border-right: 1px solid black; + border-left: 1px solid black; border-radius: 5px; box-shadow: 0 0 5px rgba(0,0,0,0.5); background-color: #B0C4DE; diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 5f768daccb7..f6d755be592 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -362,7 +362,7 @@ def _parse_labels(labels): for token in shlex.split(labels): if token[0] != "~" and token[0] != "@": if token in parsed_labels: - raise ValueError("labels string is not corect. " + + raise ValueError("labels string is not corect. " + "`{}` label is specified at least twice.".format(token)) parsed_labels[token] = {} @@ -388,7 +388,7 @@ def _parse_labels(labels): "`{}` attribute has incorrect format.".format(attr['name'])) if attr['name'] in parsed_labels[last_label]: - raise ValueError("labels string is not corect. " + + raise ValueError("labels string is not corect. " + "`{}` attribute is specified at least twice.".format(attr['name'])) parsed_labels[last_label][attr['name']] = attr @@ -544,7 +544,6 @@ def _find_and_compress_images(upload_dir, output_dir, db_task, compress_quality, filenames.sort() if len(filenames): - compressed_names = [] for idx, name in enumerate(filenames): job.meta['status'] = 'Images are being compressed.. {}%'.format(idx * 100 // len(filenames)) job.save_meta() @@ -554,10 +553,12 @@ def _find_and_compress_images(upload_dir, output_dir, db_task, compress_quality, image = image.transpose(Image.ROTATE_180) image.save(compressed_name, quality=compress_quality, optimize=True) image.close() - compressed_names.append(compressed_name) if compressed_name != name: os.remove(name) - filenames = compressed_names + # PIL::save uses filename in order to define image extension. + # We need save it as jpeg for compression and after rename the file + # Else annotation file will contain invalid file names (with other extensions) + os.rename(compressed_name, name) for frame, image_orig_path in enumerate(filenames): image_dest_path = _get_frame_path(frame, output_dir) diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index 124fd0d8d4a..2b12d634745 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -375,11 +375,12 @@ + +
+ + +
-
- - -