Automatic bordering feature during drawing/editing (#997)

main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent c7052842a7
commit f7df747cdf

@ -583,7 +583,7 @@ function buildAnnotationUI(jobData, taskData, imageMetaData, annotationData, ann
const shapeCreatorController = new ShapeCreatorController(shapeCreatorModel);
const shapeCreatorView = new ShapeCreatorView(shapeCreatorModel, shapeCreatorController);
const polyshapeEditorModel = new PolyshapeEditorModel();
const polyshapeEditorModel = new PolyshapeEditorModel(shapeCollectionModel);
const polyshapeEditorController = new PolyshapeEditorController(polyshapeEditorModel);
const polyshapeEditorView = new PolyshapeEditorView(polyshapeEditorModel,
polyshapeEditorController);

@ -0,0 +1,213 @@
/*
* Copyright (C) 2019 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* exported BorderSticker */
class BorderSticker {
constructor(currentShape, frameContent, shapes, scale) {
this._currentShape = currentShape;
this._frameContent = frameContent;
this._enabled = false;
this._groups = null;
this._scale = scale;
this._accounter = {
clicks: [],
shapeID: null,
};
const transformedShapes = shapes
.filter((shape) => !shape.model.removed)
.map((shape) => {
const pos = shape.interpolation.position;
// convert boxes to point sets
if (!('points' in pos)) {
return {
points: window.cvat.translate.points
.actualToCanvas(`${pos.xtl},${pos.ytl} ${pos.xbr},${pos.ytl}`
+ ` ${pos.xbr},${pos.ybr} ${pos.xtl},${pos.ybr}`),
color: shape.model.color.shape,
};
}
return {
points: window.cvat.translate.points
.actualToCanvas(pos.points),
color: shape.model.color.shape,
};
});
this._drawBorderMarkers(transformedShapes);
}
_addRawPoint(x, y) {
this._currentShape.array().valueOf().pop();
this._currentShape.array().valueOf().push([x, y]);
// not error, specific of the library
this._currentShape.array().valueOf().push([x, y]);
const paintHandler = this._currentShape.remember('_paintHandler');
paintHandler.drawCircles();
paintHandler.set.members.forEach((el) => {
el.attr('stroke-width', 1 / this._scale).attr('r', 2.5 / this._scale);
});
this._currentShape.plot(this._currentShape.array().valueOf());
}
_drawBorderMarkers(shapes) {
const namespace = 'http://www.w3.org/2000/svg';
this._groups = shapes.reduce((acc, shape, shapeID) => {
// Group all points by inside svg groups
const group = window.document.createElementNS(namespace, 'g');
shape.points.split(/\s/).map((point, pointID, points) => {
const [x, y] = point.split(',').map((coordinate) => +coordinate);
const circle = window.document.createElementNS(namespace, 'circle');
circle.classList.add('shape-creator-border-point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', 1 / this._scale);
circle.setAttribute('cx', +x);
circle.setAttribute('cy', +y);
circle.setAttribute('r', 5 / this._scale);
circle.doubleClickListener = (e) => {
// Just for convenience (prevent screen fit feature)
e.stopPropagation();
};
circle.clickListener = (e) => {
e.stopPropagation();
// another shape was clicked
if (this._accounter.shapeID !== null && this._accounter.shapeID !== shapeID) {
this.reset();
}
this._accounter.shapeID = shapeID;
if (this._accounter.clicks[1] === pointID) {
// the same point repeated two times
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
this._addRawPoint(_x, _y);
this.reset();
return;
}
// the first point can not be clicked twice
if (this._accounter.clicks[0] !== pointID) {
this._accounter.clicks.push(pointID);
} else {
return;
}
// up clicked group for convenience
this._frameContent.node.appendChild(group);
// the first click
if (this._accounter.clicks.length === 1) {
// draw and remove initial point just to initialize data structures
if (!this._currentShape.remember('_paintHandler').startPoint) {
this._currentShape.draw('point', e);
this._currentShape.draw('undo');
}
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
this._addRawPoint(_x, _y);
// the second click
} else if (this._accounter.clicks.length === 2) {
circle.classList.add('shape-creator-border-point-direction');
// the third click
} else {
// sign defines bypass direction
const landmarks = this._accounter.clicks;
const sign = Math.sign(landmarks[2] - landmarks[0])
* Math.sign(landmarks[1] - landmarks[0])
* Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]);
if (i === this._accounter.clicks[this._accounter.clicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
}
// remove the latest cursor position from drawing array
for (const wayPoint of way) {
const [_x, _y] = wayPoint.split(',').map((coordinate) => +coordinate);
this._addRawPoint(_x, _y);
}
this.reset();
}
};
circle.addEventListener('click', circle.clickListener);
circle.addEventListener('dblclick', circle.doubleClickListener);
return circle;
}).forEach((circle) => group.appendChild(circle));
acc.push(group);
return acc;
}, []);
this._groups
.forEach((group) => this._frameContent.node.appendChild(group));
}
reset() {
if (this._accounter.shapeID !== null) {
while (this._accounter.clicks.length > 0) {
const resetID = this._accounter.clicks.pop();
this._groups[this._accounter.shapeID]
.children[resetID].classList.remove('shape-creator-border-point-direction');
}
}
this._accounter = {
clicks: [],
shapeID: null,
};
}
disable() {
if (this._groups) {
this._groups.forEach((group) => {
Array.from(group.children).forEach((circle) => {
circle.removeEventListener('click', circle.clickListener);
circle.removeEventListener('dblclick', circle.doubleClickListener);
});
group.remove();
});
this._groups = null;
}
}
scale(scale) {
this._scale = scale;
if (this._groups) {
this._groups.forEach((group) => {
Array.from(group.children).forEach((circle) => {
circle.setAttribute('r', 5 / scale);
circle.setAttribute('stroke-width', 1 / scale);
});
});
}
}
}

@ -12,16 +12,18 @@
PolyShapeModel:false
STROKE_WIDTH:false
SVG:false
BorderSticker:false
*/
"use strict";
class PolyshapeEditorModel extends Listener {
constructor() {
constructor(shapeCollection) {
super("onPolyshapeEditorUpdate", () => this);
this._modeName = 'poly_editing';
this._active = false;
this._shapeCollection = shapeCollection;
this._data = {
points: null,
color: null,
@ -29,10 +31,12 @@ class PolyshapeEditorModel extends Listener {
oncomplete: null,
type: null,
event: null,
startPoint: null,
id: null,
};
}
edit(type, points, color, start, event, oncomplete) {
edit(type, points, color, start, startPoint, e, oncomplete, id) {
if (!this._active && !window.cvat.mode) {
window.cvat.mode = this._modeName;
this._active = true;
@ -41,7 +45,9 @@ class PolyshapeEditorModel extends Listener {
this._data.start = start;
this._data.oncomplete = oncomplete;
this._data.type = type;
this._data.event = event;
this._data.event = e;
this._data.startPoint = startPoint;
this._data.id = id;
this.notify();
}
else if (this._active) {
@ -73,6 +79,7 @@ class PolyshapeEditorModel extends Listener {
this._data.oncomplete = null;
this._data.type = null;
this._data.event = null;
this._data.startPoint = null;
this.notify();
}
}
@ -84,6 +91,11 @@ class PolyshapeEditorModel extends Listener {
get data() {
return this._data;
}
get currentShapes() {
this._shapeCollection.update();
return this._shapeCollection.currentShapes;
}
}
@ -99,6 +111,10 @@ class PolyshapeEditorController {
cancel() {
this._model.cancel();
}
get currentShapes() {
return this._model.currentShapes;
}
}
@ -108,14 +124,30 @@ class PolyshapeEditorView {
this._data = null;
this._frameContent = SVG.adopt($('#frameContent')[0]);
this._autoBorderingCheckbox = $('#autoBorderingCheckbox');
this._originalShapePointsGroup = null;
this._originalShapePoints = [];
this._originalShape = null;
this._correctLine = null;
this._borderSticker = null;
this._scale = window.cvat.player.geometry.scale;
this._frame = window.cvat.player.frames.current;
this._autoBorderingCheckbox.on('change.shapeEditor', (e) => {
if (this._correctLine) {
if (!e.target.checked) {
this._borderSticker.disable();
this._borderSticker = null;
} else {
this._borderSticker = new BorderSticker(this._correctLine, this._frameContent,
this._controller.currentShapes
.filter((shape) => shape.model.id !== this._data.id),
this._scale);
}
}
});
model.subscribe(this);
}
@ -180,6 +212,16 @@ class PolyshapeEditorView {
return offset;
}
_addRawPoint(x, y) {
this._correctLine.array().valueOf().pop();
this._correctLine.array().valueOf().push([x, y]);
// not error, specific of the library
this._correctLine.array().valueOf().push([x, y]);
this._correctLine.remember('_paintHandler').drawCircles();
this._correctLine.plot(this._correctLine.array().valueOf());
this._rescaleDrawPoints();
}
_startEdit() {
this._frame = window.cvat.player.frames.current;
let strokeWidth = this._data.type === 'points' ? 0 : STROKE_WIDTH / this._scale;
@ -222,17 +264,24 @@ class PolyshapeEditorView {
}
const [x, y] = this._data.startPoint
.split(',').map((el) => +el);
let prevPoint = {
x: this._data.event.clientX,
y: this._data.event.clientY
x,
y,
};
// draw and remove initial point just to initialize data structures
this._correctLine.draw('point', this._data.event);
this._rescaleDrawPoints();
this._correctLine.draw('undo');
this._addRawPoint(x, y);
this._frameContent.on('mousemove.polyshapeEditor', (e) => {
if (e.shiftKey && this._data.type != 'points') {
let delta = Math.sqrt(Math.pow(e.clientX - prevPoint.x, 2) + Math.pow(e.clientY - prevPoint.y, 2));
let deltaTreshold = 15;
if (e.shiftKey && this._data.type !== 'points') {
const delta = Math.sqrt(Math.pow(e.clientX - prevPoint.x, 2)
+ Math.pow(e.clientY - prevPoint.y, 2));
const deltaTreshold = 15;
if (delta > deltaTreshold) {
this._correctLine.draw('point', e);
prevPoint = {
@ -246,8 +295,10 @@ class PolyshapeEditorView {
this._frameContent.on('contextmenu.polyshapeEditor', (e) => {
if (PolyShapeModel.convertStringToNumberArray(this._correctLine.attr('points')).length > 2) {
this._correctLine.draw('undo');
}
else {
if (this._borderSticker) {
this._borderSticker.reset();
}
} else {
// Finish without points argument is just cancel
this._controller.finish();
}
@ -272,9 +323,19 @@ class PolyshapeEditorView {
}).on('mouseout', () => {
instance.attr('stroke-width', STROKE_WIDTH / this._scale);
}).on('mousedown', (e) => {
if (e.which != 1) return;
if (e.which !== 1) {
return;
}
let currentPoints = PolyShapeModel.convertStringToNumberArray(this._data.points);
let correctPoints = PolyShapeModel.convertStringToNumberArray(this._correctLine.attr('points'));
// replace the latest point from the event
// (which has not precise coordinates, to precise coordinates)
let correctPoints = this._correctLine
.attr('points')
.split(/\s/)
.slice(0, -1);
correctPoints = correctPoints.concat([`${instance.attr('cx')},${instance.attr('cy')}`]).join(' ');
correctPoints = PolyShapeModel.convertStringToNumberArray(correctPoints);
let resultPoints = [];
if (this._data.type === 'polygon') {
@ -338,6 +399,14 @@ class PolyshapeEditorView {
this._controller.finish(PolyShapeModel.convertNumberArrayToString(resultPoints));
});
}
this._autoBorderingCheckbox[0].disabled = false;
$('body').on('keydown.shapeEditor', (e) => {
if (e.ctrlKey && e.keyCode === 17) {
this._autoBorderingCheckbox[0].checked = !this._borderSticker;
this._autoBorderingCheckbox.trigger('change.shapeEditor');
}
});
}
_endEdit() {
@ -361,6 +430,14 @@ class PolyshapeEditorView {
this._frameContent.off('mousemove.polyshapeEditor');
this._frameContent.off('mousedown.polyshapeEditor');
this._frameContent.off('contextmenu.polyshapeEditor');
$('body').off('keydown.shapeEditor');
this._autoBorderingCheckbox[0].checked = false;
this._autoBorderingCheckbox[0].disabled = true;
if (this._borderSticker) {
this._borderSticker.disable();
this._borderSticker = null;
}
}
@ -379,6 +456,10 @@ class PolyshapeEditorView {
if (this._scale != scale) {
this._scale = scale;
if (this._borderSticker) {
this._borderSticker.scale(this._scale);
}
let strokeWidth = this._data && this._data.type === 'points' ? 0 : STROKE_WIDTH / this._scale;
let pointRadius = POINT_RADIUS / this._scale;

@ -16,10 +16,9 @@
showMessage:false
STROKE_WIDTH:false
SVG:false
BorderSticker: false
*/
"use strict";
class ShapeCreatorModel extends Listener {
constructor(shapeCollection) {
super('onShapeCreatorUpdate', () => this);
@ -34,8 +33,8 @@ class ShapeCreatorModel extends Listener {
}
finish(result) {
let data = {};
let frame = window.cvat.player.frames.current;
const data = {};
const frame = window.cvat.player.frames.current;
data.label_id = this._defaultLabel;
data.group = 0;
@ -50,7 +49,7 @@ class ShapeCreatorModel extends Listener {
mode: this._defaultMode,
type: this._defaultType,
label: this._defaultLabel,
frame: frame,
frame,
});
}
@ -64,7 +63,7 @@ class ShapeCreatorModel extends Listener {
this._shapeCollection.add(data, `annotation_${this._defaultType}`);
}
let model = this._shapeCollection.shapes.slice(-1)[0];
const model = this._shapeCollection.shapes.slice(-1)[0];
// Undo/redo code
window.cvat.addAction('Draw Object', () => {
@ -87,12 +86,10 @@ class ShapeCreatorModel extends Listener {
if (this._createMode) {
this._createEvent = Logger.addContinuedEvent(Logger.EventType.drawObject);
window.cvat.mode = 'creation';
}
else if (window.cvat.mode === 'creation') {
} else if (window.cvat.mode === 'creation') {
window.cvat.mode = null;
}
}
else {
} else {
this._createMode = false;
if (window.cvat.mode === 'creation') {
window.cvat.mode = null;
@ -106,6 +103,11 @@ class ShapeCreatorModel extends Listener {
this.notify();
}
get currentShapes() {
this._shapeCollection.update();
return this._shapeCollection.currentShapes;
}
get saveCurrent() {
return this._saveCurrent;
}
@ -177,6 +179,10 @@ class ShapeCreatorController {
finish(result) {
this._model.finish(result);
}
get currentShapes() {
return this._model.currentShapes;
}
}
class ShapeCreatorView {
@ -188,6 +194,7 @@ class ShapeCreatorView {
this._modeSelector = $('#shapeModeSelector');
this._typeSelector = $('#shapeTypeSelector');
this._polyShapeSizeInput = $('#polyShapeSize');
this._autoBorderingCheckbox = $('#autoBorderingCheckbox');
this._frameContent = SVG.adopt($('#frameContent')[0]);
this._frameText = SVG.adopt($("#frameText")[0]);
this._playerFrame = $('#playerFrame');
@ -203,6 +210,7 @@ class ShapeCreatorView {
this._mode = null;
this._cancel = false;
this._scale = 1;
this._borderSticker = null;
let shortkeys = window.cvat.config.shortkeys;
this._createButton.attr('title', `
@ -288,8 +296,19 @@ class ShapeCreatorView {
}
}
});
}
this._autoBorderingCheckbox.on('change.shapeCreator', (e) => {
if (this._drawInstance) {
if (!e.target.checked) {
this._borderSticker.disable();
this._borderSticker = null;
} else {
this._borderSticker = new BorderSticker(this._drawInstance, this._frameContent,
this._controller.currentShapes, this._scale);
}
}
});
}
_createPolyEvents() {
// If number of points for poly shape specified, use it.
@ -340,10 +359,21 @@ class ShapeCreatorView {
numberOfPoints ++;
});
this._autoBorderingCheckbox[0].disabled = false;
$('body').on('keydown.shapeCreator', (e) => {
if (e.ctrlKey && e.keyCode === 17) {
this._autoBorderingCheckbox[0].checked = !this._borderSticker;
this._autoBorderingCheckbox.trigger('change.shapeCreator');
}
});
this._frameContent.on('mousedown.shapeCreator', (e) => {
if (e.which === 3) {
let lenBefore = this._drawInstance.array().value.length;
this._drawInstance.draw('undo');
if (this._borderSticker) {
this._borderSticker.reset();
}
let lenAfter = this._drawInstance.array().value.length;
if (lenBefore != lenAfter) {
numberOfPoints --;
@ -373,6 +403,13 @@ class ShapeCreatorView {
this._drawInstance.on('drawstop', () => {
this._frameContent.off('mousedown.shapeCreator');
this._frameContent.off('mousemove.shapeCreator');
this._autoBorderingCheckbox[0].disabled = true;
this._autoBorderingCheckbox[0].checked = false;
$('body').off('keydown.shapeCreator');
if (this._borderSticker) {
this._borderSticker.disable();
this._borderSticker = null;
}
});
// Also we need callback on drawdone event for get points
this._drawInstance.on('drawdone', function(e) {
@ -418,7 +455,7 @@ class ShapeCreatorView {
let sizeUI = null;
switch(this._type) {
case 'box':
this._drawInstance = this._frameContent.rect().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({
this._drawInstance = this._frameContent.rect().draw({ snapToGrid: 0.1 }).addClass('shapeCreation').attr({
'stroke-width': STROKE_WIDTH / this._scale,
}).on('drawstop', function(e) {
if (this._cancel) return;
@ -461,9 +498,10 @@ class ShapeCreatorView {
});
break;
case 'points':
this._drawInstance = this._frameContent.polyline().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({
'stroke-width': 0,
});
this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 })
.addClass('shapeCreation').attr({
'stroke-width': 0,
});
this._createPolyEvents();
break;
case 'polygon':
@ -474,9 +512,10 @@ class ShapeCreatorView {
this._controller.switchCreateMode(true);
return;
}
this._drawInstance = this._frameContent.polygon().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({
'stroke-width': STROKE_WIDTH / this._scale,
});
this._drawInstance = this._frameContent.polygon().draw({ snapToGrid: 0.1 })
.addClass('shapeCreation').attr({
'stroke-width': STROKE_WIDTH / this._scale,
});
this._createPolyEvents();
break;
case 'polyline':
@ -487,9 +526,10 @@ class ShapeCreatorView {
this._controller.switchCreateMode(true);
return;
}
this._drawInstance = this._frameContent.polyline().draw({snapToGrid: 0.1}).addClass('shapeCreation').attr({
'stroke-width': STROKE_WIDTH / this._scale,
});
this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 })
.addClass('shapeCreation').attr({
'stroke-width': STROKE_WIDTH / this._scale,
});
this._createPolyEvents();
break;
default:
@ -585,6 +625,9 @@ class ShapeCreatorView {
this._scale = player.geometry.scale;
if (this._drawInstance) {
this._rescaleDrawPoints();
if (this._borderSticker) {
this._borderSticker.scale(this._scale);
}
if (this._aim) {
this._aim.x.attr('stroke-width', STROKE_WIDTH / this._scale);
this._aim.y.attr('stroke-width', STROKE_WIDTH / this._scale);

@ -3007,7 +3007,8 @@ class PolyShapeView extends ShapeView {
// Run edit mode
PolyShapeView.editor.edit(this._controller.type.split('_')[1],
this._uis.shape.attr('points'), this._color, index, e,
this._uis.shape.attr('points'), this._color, index,
this._uis.shape.attr('points').split(/\s/)[index], e,
(points) => {
this._uis.shape.removeClass('hidden');
if (this._uis.points) {
@ -3017,7 +3018,8 @@ class PolyShapeView extends ShapeView {
this._uis.shape.attr('points', points);
this._controller.updatePosition(window.cvat.player.frames.current, this._buildPosition());
}
}
},
this._controller.id
);
}
}

@ -224,6 +224,24 @@
cursor: w-resize;
}
.shape-creator-border-point {
opacity: 0.55;
}
.shape-creator-border-point:hover {
opacity: 1;
fill: red;
}
.shape-creator-border-point:active {
opacity: 0.55;
fill: red;
}
.shape-creator-border-point-direction {
fill: blueviolet;
}
.shapeText {
font-size: 0.12em;
fill: white;

@ -41,6 +41,7 @@
<script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/borderSticker.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/coordinateTranslator.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/labelsInfo.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/annotationParser.js' %}"></script>
@ -191,6 +192,10 @@
<label style="margin-right: 10px;"> Label </label>
<input type="radio" name="colorByRadio" id="colorByLabelRadio" class="settingsBox"/>
</div>
<div style="float: left; margin-left: 50px;" title="Press Ctrl to switch">
<label style="margin-right: 10px;" for="autoBorderingCheckbox"> Auto bordering </label>
<input type="checkbox" id="autoBorderingCheckbox" class="settingsBox" disabled/>
</div>
</td>
</tr>
</table>

Loading…
Cancel
Save