Skip to content

Commit 3c645b2

Browse files
bsekachevChris Lee-Messer
authored and
Chris Lee-Messer
committed
Automatic bordering feature during drawing/editing (cvat-ai#997)
1 parent 0e2b262 commit 3c645b2

File tree

7 files changed

+399
-37
lines changed

7 files changed

+399
-37
lines changed

cvat/apps/engine/static/engine/js/annotationUI.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ function buildAnnotationUI(jobData, taskData, imageMetaData, annotationData, ann
583583
const shapeCreatorController = new ShapeCreatorController(shapeCreatorModel);
584584
const shapeCreatorView = new ShapeCreatorView(shapeCreatorModel, shapeCreatorController);
585585

586-
const polyshapeEditorModel = new PolyshapeEditorModel();
586+
const polyshapeEditorModel = new PolyshapeEditorModel(shapeCollectionModel);
587587
const polyshapeEditorController = new PolyshapeEditorController(polyshapeEditorModel);
588588
const polyshapeEditorView = new PolyshapeEditorView(polyshapeEditorModel,
589589
polyshapeEditorController);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright (C) 2019 Intel Corporation
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
/* exported BorderSticker */
8+
9+
class BorderSticker {
10+
constructor(currentShape, frameContent, shapes, scale) {
11+
this._currentShape = currentShape;
12+
this._frameContent = frameContent;
13+
this._enabled = false;
14+
this._groups = null;
15+
this._scale = scale;
16+
this._accounter = {
17+
clicks: [],
18+
shapeID: null,
19+
};
20+
21+
const transformedShapes = shapes
22+
.filter((shape) => !shape.model.removed)
23+
.map((shape) => {
24+
const pos = shape.interpolation.position;
25+
// convert boxes to point sets
26+
if (!('points' in pos)) {
27+
return {
28+
points: window.cvat.translate.points
29+
.actualToCanvas(`${pos.xtl},${pos.ytl} ${pos.xbr},${pos.ytl}`
30+
+ ` ${pos.xbr},${pos.ybr} ${pos.xtl},${pos.ybr}`),
31+
color: shape.model.color.shape,
32+
};
33+
}
34+
35+
return {
36+
points: window.cvat.translate.points
37+
.actualToCanvas(pos.points),
38+
color: shape.model.color.shape,
39+
};
40+
});
41+
42+
this._drawBorderMarkers(transformedShapes);
43+
}
44+
45+
_addRawPoint(x, y) {
46+
this._currentShape.array().valueOf().pop();
47+
this._currentShape.array().valueOf().push([x, y]);
48+
// not error, specific of the library
49+
this._currentShape.array().valueOf().push([x, y]);
50+
const paintHandler = this._currentShape.remember('_paintHandler');
51+
paintHandler.drawCircles();
52+
paintHandler.set.members.forEach((el) => {
53+
el.attr('stroke-width', 1 / this._scale).attr('r', 2.5 / this._scale);
54+
});
55+
this._currentShape.plot(this._currentShape.array().valueOf());
56+
}
57+
58+
_drawBorderMarkers(shapes) {
59+
const namespace = 'http://www.w3.org/2000/svg';
60+
61+
this._groups = shapes.reduce((acc, shape, shapeID) => {
62+
// Group all points by inside svg groups
63+
const group = window.document.createElementNS(namespace, 'g');
64+
shape.points.split(/\s/).map((point, pointID, points) => {
65+
const [x, y] = point.split(',').map((coordinate) => +coordinate);
66+
const circle = window.document.createElementNS(namespace, 'circle');
67+
circle.classList.add('shape-creator-border-point');
68+
circle.setAttribute('fill', shape.color);
69+
circle.setAttribute('stroke', 'black');
70+
circle.setAttribute('stroke-width', 1 / this._scale);
71+
circle.setAttribute('cx', +x);
72+
circle.setAttribute('cy', +y);
73+
circle.setAttribute('r', 5 / this._scale);
74+
75+
circle.doubleClickListener = (e) => {
76+
// Just for convenience (prevent screen fit feature)
77+
e.stopPropagation();
78+
};
79+
80+
circle.clickListener = (e) => {
81+
e.stopPropagation();
82+
// another shape was clicked
83+
if (this._accounter.shapeID !== null && this._accounter.shapeID !== shapeID) {
84+
this.reset();
85+
}
86+
87+
this._accounter.shapeID = shapeID;
88+
89+
if (this._accounter.clicks[1] === pointID) {
90+
// the same point repeated two times
91+
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
92+
this._addRawPoint(_x, _y);
93+
this.reset();
94+
return;
95+
}
96+
97+
// the first point can not be clicked twice
98+
if (this._accounter.clicks[0] !== pointID) {
99+
this._accounter.clicks.push(pointID);
100+
} else {
101+
return;
102+
}
103+
104+
// up clicked group for convenience
105+
this._frameContent.node.appendChild(group);
106+
107+
// the first click
108+
if (this._accounter.clicks.length === 1) {
109+
// draw and remove initial point just to initialize data structures
110+
if (!this._currentShape.remember('_paintHandler').startPoint) {
111+
this._currentShape.draw('point', e);
112+
this._currentShape.draw('undo');
113+
}
114+
115+
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
116+
this._addRawPoint(_x, _y);
117+
// the second click
118+
} else if (this._accounter.clicks.length === 2) {
119+
circle.classList.add('shape-creator-border-point-direction');
120+
// the third click
121+
} else {
122+
// sign defines bypass direction
123+
const landmarks = this._accounter.clicks;
124+
const sign = Math.sign(landmarks[2] - landmarks[0])
125+
* Math.sign(landmarks[1] - landmarks[0])
126+
* Math.sign(landmarks[2] - landmarks[1]);
127+
128+
// go via a polygon and get vertexes
129+
// the first vertex has been already drawn
130+
const way = [];
131+
for (let i = landmarks[0] + sign; ; i += sign) {
132+
if (i < 0) {
133+
i = points.length - 1;
134+
} else if (i === points.length) {
135+
i = 0;
136+
}
137+
138+
way.push(points[i]);
139+
140+
if (i === this._accounter.clicks[this._accounter.clicks.length - 1]) {
141+
// put the last element twice
142+
// specific of svg.draw.js
143+
// way.push(points[i]);
144+
break;
145+
}
146+
}
147+
148+
// remove the latest cursor position from drawing array
149+
for (const wayPoint of way) {
150+
const [_x, _y] = wayPoint.split(',').map((coordinate) => +coordinate);
151+
this._addRawPoint(_x, _y);
152+
}
153+
154+
this.reset();
155+
}
156+
};
157+
158+
circle.addEventListener('click', circle.clickListener);
159+
circle.addEventListener('dblclick', circle.doubleClickListener);
160+
161+
return circle;
162+
}).forEach((circle) => group.appendChild(circle));
163+
164+
acc.push(group);
165+
return acc;
166+
}, []);
167+
168+
this._groups
169+
.forEach((group) => this._frameContent.node.appendChild(group));
170+
}
171+
172+
reset() {
173+
if (this._accounter.shapeID !== null) {
174+
while (this._accounter.clicks.length > 0) {
175+
const resetID = this._accounter.clicks.pop();
176+
this._groups[this._accounter.shapeID]
177+
.children[resetID].classList.remove('shape-creator-border-point-direction');
178+
}
179+
}
180+
181+
this._accounter = {
182+
clicks: [],
183+
shapeID: null,
184+
};
185+
}
186+
187+
disable() {
188+
if (this._groups) {
189+
this._groups.forEach((group) => {
190+
Array.from(group.children).forEach((circle) => {
191+
circle.removeEventListener('click', circle.clickListener);
192+
circle.removeEventListener('dblclick', circle.doubleClickListener);
193+
});
194+
195+
group.remove();
196+
});
197+
198+
this._groups = null;
199+
}
200+
}
201+
202+
scale(scale) {
203+
this._scale = scale;
204+
if (this._groups) {
205+
this._groups.forEach((group) => {
206+
Array.from(group.children).forEach((circle) => {
207+
circle.setAttribute('r', 5 / scale);
208+
circle.setAttribute('stroke-width', 1 / scale);
209+
});
210+
});
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)