diff --git a/cvat/apps/annotation/cvat.py b/cvat/apps/annotation/cvat.py
index ec687812..5e7b3fe3 100644
--- a/cvat/apps/annotation/cvat.py
+++ b/cvat/apps/annotation/cvat.py
@@ -119,6 +119,11 @@ def create_xml_dumper(file_object):
self.xmlgen.startElement("points", points)
self._level += 1
+ def open_cuboid(self, cuboid):
+ self._indent()
+ self.xmlgen.startElement("cuboid", cuboid)
+ self._level += 1
+
def add_attribute(self, attribute):
self._indent()
self.xmlgen.startElement("attribute", {"name": attribute["name"]})
@@ -145,6 +150,11 @@ def create_xml_dumper(file_object):
self._indent()
self.xmlgen.endElement("points")
+ def close_cuboid(self):
+ self._level -= 1
+ self._indent()
+ self.xmlgen.endElement("cuboid")
+
def close_image(self):
self._level -= 1
self._indent()
@@ -191,6 +201,25 @@ def dump_as_cvat_annotation(file_object, annotations):
("xbr", "{:.2f}".format(shape.points[2])),
("ybr", "{:.2f}".format(shape.points[3]))
]))
+ elif shape.type == "cuboid":
+ dump_data.update(OrderedDict([
+ ("xtl1", "{:.2f}".format(shape.points[0])),
+ ("ytl1", "{:.2f}".format(shape.points[1])),
+ ("xbl1", "{:.2f}".format(shape.points[2])),
+ ("ybl1", "{:.2f}".format(shape.points[3])),
+ ("xtr1", "{:.2f}".format(shape.points[4])),
+ ("ytr1", "{:.2f}".format(shape.points[5])),
+ ("xbr1", "{:.2f}".format(shape.points[6])),
+ ("ybr1", "{:.2f}".format(shape.points[7])),
+ ("xtl2", "{:.2f}".format(shape.points[8])),
+ ("ytl2", "{:.2f}".format(shape.points[9])),
+ ("xbl2", "{:.2f}".format(shape.points[10])),
+ ("ybl2", "{:.2f}".format(shape.points[11])),
+ ("xtr2", "{:.2f}".format(shape.points[12])),
+ ("ytr2", "{:.2f}".format(shape.points[13])),
+ ("xbr2", "{:.2f}".format(shape.points[14])),
+ ("ybr2", "{:.2f}".format(shape.points[15]))
+ ]))
else:
dump_data.update(OrderedDict([
("points", ';'.join((
@@ -206,6 +235,7 @@ def dump_as_cvat_annotation(file_object, annotations):
if shape.group:
dump_data['group_id'] = str(shape.group)
+
if shape.type == "rectangle":
dumper.open_box(dump_data)
elif shape.type == "polygon":
@@ -214,6 +244,8 @@ def dump_as_cvat_annotation(file_object, annotations):
dumper.open_polyline(dump_data)
elif shape.type == "points":
dumper.open_points(dump_data)
+ elif shape.type == "cuboid":
+ dumper.open_cuboid(dump_data)
else:
raise NotImplementedError("unknown shape type")
@@ -231,6 +263,8 @@ def dump_as_cvat_annotation(file_object, annotations):
dumper.close_polyline()
elif shape.type == "points":
dumper.close_points()
+ elif shape.type == "cuboid":
+ dumper.close_cuboid()
else:
raise NotImplementedError("unknown shape type")
@@ -268,6 +302,25 @@ def dump_as_cvat_interpolation(file_object, annotations):
("xbr", "{:.2f}".format(shape.points[2])),
("ybr", "{:.2f}".format(shape.points[3])),
]))
+ elif shape.type == "cuboid":
+ dump_data.update(OrderedDict([
+ ("xtl1", "{:.2f}".format(shape.points[0])),
+ ("ytl1", "{:.2f}".format(shape.points[1])),
+ ("xbl1", "{:.2f}".format(shape.points[2])),
+ ("ybl1", "{:.2f}".format(shape.points[3])),
+ ("xtr1", "{:.2f}".format(shape.points[4])),
+ ("ytr1", "{:.2f}".format(shape.points[5])),
+ ("xbr1", "{:.2f}".format(shape.points[6])),
+ ("ybr1", "{:.2f}".format(shape.points[7])),
+ ("xtl2", "{:.2f}".format(shape.points[8])),
+ ("ytl2", "{:.2f}".format(shape.points[9])),
+ ("xbl2", "{:.2f}".format(shape.points[10])),
+ ("ybl2", "{:.2f}".format(shape.points[11])),
+ ("xtr2", "{:.2f}".format(shape.points[12])),
+ ("ytr2", "{:.2f}".format(shape.points[13])),
+ ("xbr2", "{:.2f}".format(shape.points[14])),
+ ("ybr2", "{:.2f}".format(shape.points[15]))
+ ]))
else:
dump_data.update(OrderedDict([
("points", ';'.join(['{:.2f},{:.2f}'.format(x, y)
@@ -285,6 +338,8 @@ def dump_as_cvat_interpolation(file_object, annotations):
dumper.open_polyline(dump_data)
elif shape.type == "points":
dumper.open_points(dump_data)
+ elif shape.type == "cuboid":
+ dumper.open_cuboid(dump_data)
else:
raise NotImplementedError("unknown shape type")
@@ -302,6 +357,8 @@ def dump_as_cvat_interpolation(file_object, annotations):
dumper.close_polyline()
elif shape.type == "points":
dumper.close_points()
+ elif shape.type == "cuboid":
+ dumper.close_cuboid()
else:
raise NotImplementedError("unknown shape type")
dumper.close_track()
@@ -347,7 +404,7 @@ def load(file_object, annotations):
context = iter(context)
ev, _ = next(context)
- supported_shapes = ('box', 'polygon', 'polyline', 'points')
+ supported_shapes = ('box', 'polygon', 'polyline', 'points', 'cuboid')
track = None
shape = None
@@ -393,6 +450,24 @@ def load(file_object, annotations):
shape['points'].append(el.attrib['ytl'])
shape['points'].append(el.attrib['xbr'])
shape['points'].append(el.attrib['ybr'])
+ elif el.tag == 'cuboid':
+ shape['points'].append(el.attrib['xtl1'])
+ shape['points'].append(el.attrib['ytl1'])
+ shape['points'].append(el.attrib['xbl1'])
+ shape['points'].append(el.attrib['ybl1'])
+ shape['points'].append(el.attrib['xtr1'])
+ shape['points'].append(el.attrib['ytr1'])
+ shape['points'].append(el.attrib['xbr1'])
+ shape['points'].append(el.attrib['ybr1'])
+
+ shape['points'].append(el.attrib['xtl2'])
+ shape['points'].append(el.attrib['ytl2'])
+ shape['points'].append(el.attrib['xbl2'])
+ shape['points'].append(el.attrib['ybl2'])
+ shape['points'].append(el.attrib['xtr2'])
+ shape['points'].append(el.attrib['ytr2'])
+ shape['points'].append(el.attrib['xbr2'])
+ shape['points'].append(el.attrib['ybr2'])
else:
for pair in el.attrib['points'].split(';'):
shape['points'].extend(map(float, pair.split(',')))
diff --git a/cvat/apps/dextr_segmentation/static/dextr_segmentation/js/enginePlugin.js b/cvat/apps/dextr_segmentation/static/dextr_segmentation/js/enginePlugin.js
index 7384f33b..0869ba8d 100644
--- a/cvat/apps/dextr_segmentation/static/dextr_segmentation/js/enginePlugin.js
+++ b/cvat/apps/dextr_segmentation/static/dextr_segmentation/js/enginePlugin.js
@@ -55,7 +55,7 @@ window.addEventListener('DOMContentLoaded', () => {
get: () => instance._defaultType,
set: (type) => {
if (!['box', 'box_by_4_points', 'points', 'polygon',
- 'polyline', 'auto_segmentation'].includes(type)) {
+ 'polyline', 'auto_segmentation', 'cuboid'].includes(type)) {
throw Error(`Unknown shape type found ${type}`);
}
instance._defaultType = type;
diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py
index d2786795..a513cf5a 100644
--- a/cvat/apps/engine/models.py
+++ b/cvat/apps/engine/models.py
@@ -249,6 +249,7 @@ class ShapeType(str, Enum):
POLYGON = 'polygon' # (x0, y0, ..., xn, yn)
POLYLINE = 'polyline' # (x0, y0, ..., xn, yn)
POINTS = 'points' # (x0, y0, ..., xn, yn)
+ CUBOID = 'cuboid'
@classmethod
def choices(self):
diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py
index 8d32c081..e225dd23 100644
--- a/cvat/apps/engine/serializers.py
+++ b/cvat/apps/engine/serializers.py
@@ -373,7 +373,7 @@ class ShapeSerializer(serializers.Serializer):
occluded = serializers.BooleanField()
z_order = serializers.IntegerField(default=0)
points = serializers.ListField(
- child=serializers.FloatField(min_value=0),
+ child=serializers.FloatField(),
allow_empty=False,
)
diff --git a/cvat/apps/engine/static/engine/js/annotationParser.js b/cvat/apps/engine/static/engine/js/annotationParser.js
index 6ea4d340..db3538a3 100644
--- a/cvat/apps/engine/static/engine/js/annotationParser.js
+++ b/cvat/apps/engine/static/engine/js/annotationParser.js
@@ -29,10 +29,10 @@ class AnnotationParser {
const imWidth = this._im_meta[frame].width;
const imHeight = this._im_meta[frame].height;
- let xtl = +box.getAttribute('xtl');
- let ytl = +box.getAttribute('ytl');
- let xbr = +box.getAttribute('xbr');
- let ybr = +box.getAttribute('ybr');
+ const xtl = +box.getAttribute("xtl");
+ const ytl = +box.getAttribute("ytl");
+ const xbr = +box.getAttribute("xbr");
+ const ybr = +box.getAttribute("ybr");
if (xtl < 0 || ytl < 0 || xbr < 0 || ybr < 0
|| xtl > imWidth || ytl > imHeight || xbr > imWidth || ybr > imHeight) {
@@ -155,6 +155,7 @@ class AnnotationParser {
polygons: [],
polylines: [],
points: [],
+ cuboids: [],
};
const tracks = xml.getElementsByTagName('track');
@@ -163,12 +164,14 @@ class AnnotationParser {
polygon: this._getShapeFromPath('polygon', tracks),
polyline: this._getShapeFromPath('polyline', tracks),
points: this._getShapeFromPath('points', tracks),
+ cuboid: this._getShapeFromPath('cuboid', tracks),
};
const shapeTarget = {
box: 'boxes',
polygon: 'polygons',
polyline: 'polylines',
points: 'points',
+ cuboid: 'cuboids',
};
const images = xml.getElementsByTagName('image');
@@ -194,6 +197,10 @@ class AnnotationParser {
points.setAttribute('frame', frame);
parsed.points.push(points);
}
+ for (const cuboid of image.getElementsByTagName('cuboid')) {
+ cuboid.setAttribute('frame', frame);
+ parsed.cuboid.push(cuboid);
+ }
}
for (const shapeType in parsed) {
@@ -224,6 +231,18 @@ class AnnotationParser {
occluded,
points,
});
+ } else if (shapeType === 'cuboid') {
+ const [points, occluded, zOrder] = this._getPolyPosition(shape, frame);
+ data[shapeTarget[shapeType]].push({
+ label_id: labelId,
+ group: +group,
+ attributes: attributeList,
+ type: 'cuboid',
+ z_order: zOrder,
+ frame,
+ occluded,
+ points,
+ });
} else {
const [points, occluded, zOrder] = this._getPolyPosition(shape, frame);
data[shapeTarget[shapeType]].push({
diff --git a/cvat/apps/engine/static/engine/js/annotationSaver.js b/cvat/apps/engine/static/engine/js/annotationSaver.js
index a1074bc0..87b813fa 100644
--- a/cvat/apps/engine/static/engine/js/annotationSaver.js
+++ b/cvat/apps/engine/static/engine/js/annotationSaver.js
@@ -1,3 +1,9 @@
+/*
+ * Copyright (C) 2018 Intel Corporation
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
/* exported buildAnnotationSaver */
/* global
diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js
index b1469728..5ce8f5f8 100644
--- a/cvat/apps/engine/static/engine/js/annotationUI.js
+++ b/cvat/apps/engine/static/engine/js/annotationUI.js
@@ -295,6 +295,8 @@ function setupMenu(job, task, shapeCollectionModel,
${byLabelsStat[labelId].polylines.interpolation} |
${byLabelsStat[labelId].points.annotation} |
${byLabelsStat[labelId].points.interpolation} |
+ ${byLabelsStat[labelId].cuboids.annotation} |
+ ${byLabelsStat[labelId].cuboids.interpolation} |
${byLabelsStat[labelId].manually} |
${byLabelsStat[labelId].interpolated} |
${byLabelsStat[labelId].total} |
@@ -312,6 +314,8 @@ function setupMenu(job, task, shapeCollectionModel,
${totalStat.polylines.interpolation} |
${totalStat.points.annotation} |
${totalStat.points.interpolation} |
+ ${totalStat.cuboids.annotation} |
+ ${totalStat.cuboids.interpolation} |
${totalStat.manually} |
${totalStat.interpolated} |
${totalStat.total} |
@@ -670,13 +674,15 @@ function buildAnnotationUI(jobData, taskData, imageMetaData, annotationData, ann
'track count': totalStat.boxes.annotation + totalStat.boxes.interpolation
+ totalStat.polygons.annotation + totalStat.polygons.interpolation
+ totalStat.polylines.annotation + totalStat.polylines.interpolation
- + totalStat.points.annotation + totalStat.points.interpolation,
+ + totalStat.points.annotation + totalStat.points.interpolation
+ + totalStat.cuboids.annotation + totalStat.cuboids.interpolation,
'frame count': window.cvat.player.frames.stop - window.cvat.player.frames.start + 1,
'object count': totalStat.total,
'box count': totalStat.boxes.annotation + totalStat.boxes.interpolation,
'polygon count': totalStat.polygons.annotation + totalStat.polygons.interpolation,
'polyline count': totalStat.polylines.annotation + totalStat.polylines.interpolation,
'points count': totalStat.points.annotation + totalStat.points.interpolation,
+ 'cuboid count': totalStat.cuboids.annotation + totalStat.cuboids.interpolation,
});
loadJobEvent.close();
diff --git a/cvat/apps/engine/static/engine/js/cuboidShape.js b/cvat/apps/engine/static/engine/js/cuboidShape.js
new file mode 100644
index 00000000..88746714
--- /dev/null
+++ b/cvat/apps/engine/static/engine/js/cuboidShape.js
@@ -0,0 +1,1370 @@
+/* eslint-disable func-names */
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable curly */
+/*
+ * Copyright (C) 2018 Intel Corporation
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/* exported CuboidModel, CuboidView */
+
+/* global
+ SVG:false
+ PolylineModel:false
+ PolyShapeController:false
+ PolyShapeModel:false
+ PolyShapeView:false
+ ShapeView:false STROKE_WIDTH:false
+ AREA_TRESHOLD:false
+ POINT_RADIUS:false
+ SELECT_POINT_STROKE_WIDTH:false
+ convertToArray:false
+ convertPlainArrayToActual:false
+ intersection:false
+*/
+
+const MIN_EDGE_LENGTH = 3;
+
+const orientationEnum = {
+ LEFT: 0,
+ RIGHT: 1,
+};
+
+class Equation {
+ constructor(p1, p2) {
+ this.a = p1[1] - p2[1];
+ this.b = p2[0] - p1[0];
+ this.c = this.b * p1[1] + this.a * p1[0];
+
+ const temp = { x: p1[0], y: p1[1] };
+ const p1Canvas = window.cvat.translate.points.actualToCanvas([temp])[0];
+ this.cCanvas = this.b * p1Canvas.y + this.a * p1Canvas.x;
+ }
+
+ // get the line equation in actual coordinates
+ getY(x) {
+ return (this.c - this.a * x) / this.b;
+ }
+
+ // get the line equation in canvas coordinates
+ getYCanvas(x) {
+ return (this.cCanvas - this.a * x) / this.b;
+ }
+}
+
+class Figure {
+ constructor(indices, Vmodel) {
+ this.indices = indices;
+ this.viewmodel = Vmodel;
+ }
+
+ get points() {
+ const points = [];
+ for (const index of this.indices) {
+ points.push(this.viewmodel.points[`${index}`]);
+ }
+ return points;
+ }
+
+ // sets the point for a given edge, points must be given in
+ // array form in the same ordering as the getter
+ // if you only need to update a subset of the points,
+ // simply put null for the points you want to keep
+ set points(newPoints) {
+ const oldPoints = this.viewmodel.points;
+ for (let i = 0; i < newPoints.length; i += 1) {
+ if (newPoints[`${i}`] !== null) {
+ oldPoints[this.indices[`${i}`]] = { x: newPoints[`${i}`].x, y: newPoints[`${i}`].y };
+ }
+ }
+ }
+
+ get canvasPoints() {
+ let { points } = this;
+ points = window.cvat.translate.points.actualToCanvas(points);
+ return points;
+ }
+}
+
+class Edge extends Figure {
+ getEquation() {
+ let { points } = this;
+ points = convertToArray(points);
+ return new Equation(points[0], points[1]);
+ }
+}
+
+class Cuboid2PointViewModel {
+ constructor(points, leftFacing) {
+ this.points = points;
+ this._initEdges();
+ this._initFaces();
+ this._updateVanishingPoints();
+ this.buildBackEdge(leftFacing);
+ this.updatePoints();
+ }
+
+ getPoints() {
+ return this.points;
+ }
+
+ setPoints(points) {
+ this.points = points;
+ }
+
+ updatePoints() {
+ // making sure that the edges are vertical
+ this.fr.points[0].x = this.fr.points[1].x;
+ this.fl.points[0].x = this.fl.points[1].x;
+ this.dr.points[0].x = this.dr.points[1].x;
+ this.dl.points[0].x = this.dl.points[1].x;
+ }
+
+ computeSideEdgeConstraints(edge) {
+ const midLength = this.fr.canvasPoints[1].y - this.fr.canvasPoints[0].y - 1;
+
+ const minY = edge.canvasPoints[1].y - midLength;
+ const maxY = edge.canvasPoints[0].y + midLength;
+
+ const y1 = edge.points[0].y;
+ const y2 = edge.points[1].y;
+
+ const miny1 = y2 - midLength;
+ const maxy1 = y2 - MIN_EDGE_LENGTH;
+
+ const miny2 = y1 + MIN_EDGE_LENGTH;
+ const maxy2 = y1 + midLength;
+
+ return {
+ constraint: {
+ minY,
+ maxY,
+ },
+ y1Range: {
+ max: maxy1,
+ min: miny1,
+ },
+ y2Range: {
+ max: maxy2,
+ min: miny2,
+ },
+ };
+ }
+
+ // boolean value parameter controls which edges should be used to recalculate vanishing points
+ _updateVanishingPoints(buildright) {
+ let leftEdge = 0;
+ let rightEdge = 0;
+ let midEdge = 0;
+ if (buildright) {
+ leftEdge = convertToArray(this.fr.points);
+ rightEdge = convertToArray(this.dl.points);
+ midEdge = convertToArray(this.fl.points);
+ } else {
+ leftEdge = convertToArray(this.fl.points);
+ rightEdge = convertToArray(this.dr.points);
+ midEdge = convertToArray(this.fr.points);
+ }
+
+ this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
+ this.vpr = intersection(rightEdge[0], midEdge[0], rightEdge[1], midEdge[1]);
+ if (this.vpl === null) {
+ // shift the edge slightly to avoid edge case
+ leftEdge[0][1] -= 0.001;
+ leftEdge[0][0] += 0.001;
+ leftEdge[1][0] += 0.001;
+ this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
+ }
+ if (this.vpr === null) {
+ // shift the edge slightly to avoid edge case
+ rightEdge[0][1] -= 0.001;
+ rightEdge[0][0] -= 0.001;
+ rightEdge[1][0] -= 0.001;
+ this.vpr = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
+ }
+ }
+
+ _initEdges() {
+ this.fl = new Edge([0, 1], this);
+ this.fr = new Edge([2, 3], this);
+ this.dr = new Edge([4, 5], this);
+ this.dl = new Edge([6, 7], this);
+
+ this.ft = new Edge([0, 2], this);
+ this.lt = new Edge([0, 6], this);
+ this.rt = new Edge([2, 4], this);
+ this.dt = new Edge([6, 4], this);
+
+ this.fb = new Edge([1, 3], this);
+ this.lb = new Edge([1, 7], this);
+ this.rb = new Edge([3, 5], this);
+ this.db = new Edge([7, 5], this);
+
+ this.edgeList = [this.fl, this.fr, this.dl, this.dr, this.ft, this.lt,
+ this.rt, this.dt, this.fb, this.lb, this.rb, this.db];
+ }
+
+ _initFaces() {
+ this.front = new Figure([0, 1, 3, 2], this);
+ this.right = new Figure([2, 3, 5, 4], this);
+ this.dorsal = new Figure([4, 5, 7, 6], this);
+ this.left = new Figure([6, 7, 1, 0], this);
+ this.top = new Figure([0, 2, 4, 6], this);
+ this.bot = new Figure([1, 3, 5, 7], this);
+
+ this.facesList = [this.front, this.right, this.dorsal, this.left];
+ }
+
+ buildBackEdge(buildRight) {
+ let leftPoints = 0;
+ let rightPoints = 0;
+
+ let topIndex = 0;
+ let botIndex = 0;
+
+ if (buildRight) {
+ this._updateVanishingPoints(true);
+ leftPoints = this.dl.points;
+ rightPoints = this.fr.points;
+ topIndex = 4;
+ botIndex = 5;
+ } else {
+ this._updateVanishingPoints();
+ leftPoints = this.dr.points;
+ rightPoints = this.fl.points;
+ topIndex = 6;
+ botIndex = 7;
+ }
+
+ const vpLeft = this.vpl;
+ const vpRight = this.vpr;
+
+ leftPoints = convertToArray(leftPoints);
+ rightPoints = convertToArray(rightPoints);
+
+ let p1 = intersection(vpLeft, leftPoints[0], vpRight, rightPoints[0]);
+ let p2 = intersection(vpLeft, leftPoints[1], vpRight, rightPoints[1]);
+
+ if (p1 === null) {
+ p1 = [p2[0], vpLeft[1]];
+ } else if (p2 === null) {
+ p2 = [p1[0], vpLeft[1]];
+ }
+
+ this.points[`${topIndex}`] = { x: p1[0], y: p1[1] };
+ this.points[`${botIndex}`] = { x: p2[0], y: p2[1] };
+
+ // Making sure that the vertical edges stay vertical
+ this.updatePoints();
+ }
+
+ get vplCanvas() {
+ const { vpl } = this;
+ const vp = { x: vpl[0], y: vpl[1] };
+ return window.cvat.translate.points.actualToCanvas([vp])[0];
+ }
+
+ get vprCanvas() {
+ const { vpr } = this;
+ const vp = { x: vpr[0], y: vpr[1] };
+ return window.cvat.translate.points.actualToCanvas([vp])[0];
+ }
+}
+
+
+class CuboidController extends PolyShapeController {
+ constructor(cuboidModel) {
+ super(cuboidModel);
+ const frame = window.cvat.player.frames.current;
+ const points = PolylineModel.convertStringToNumberArray(
+ cuboidModel._interpolatePosition(frame).points,
+ );
+
+ this.viewModel = new Cuboid2PointViewModel(points);
+ this.orientation = orientationEnum.LEFT;
+ }
+
+ setView(cuboidView) {
+ this.cuboidView = cuboidView;
+ }
+
+ set draggable(value) {
+ this._model.draggable = value;
+ }
+
+ get draggable() {
+ return this._model.draggable;
+ }
+
+ addEventsToCube() {
+ const controller = this;
+ const cuboidview = this.cuboidView;
+ const edges = cuboidview._uis.shape.getEdges();
+ const grabPoints = cuboidview._uis.shape.getGrabPoints();
+ const draggableFaces = [
+ cuboidview._uis.shape.left,
+ cuboidview._uis.shape.dorsal,
+ cuboidview._uis.shape.right,
+ ];
+
+ if (this.viewModel.dl.points[0].x > this.viewModel.fl.points[0].x) {
+ this.orientation = orientationEnum.LEFT;
+ } else {
+ this.orientation = orientationEnum.RIGHT;
+ }
+
+ this.updateGrabPoints();
+ cuboidview._uis.shape.on('mousedown', () => {
+ ShapeView.prototype._positionateMenus.call(cuboidview);
+ });
+ edges.forEach((edge) => {
+ edge.on('resizestart', () => {
+ cuboidview._flags.resizing = true;
+ cuboidview._hideShapeText();
+ cuboidview.notify('resize');
+ }).on('resizedone', () => {
+ cuboidview._flags.resizing = false;
+ controller.updateModel();
+ controller.updateViewModel();
+ cuboidview.notify('resize');
+ });
+ });
+ grabPoints.forEach((grabPoint) => {
+ grabPoint.on('dragstart', () => {
+ cuboidview._flags.dragging = true;
+ cuboidview._hideShapeText();
+ cuboidview.notify('drag');
+ }).on('dragend', () => {
+ cuboidview._flags.dragging = false;
+ cuboidview._showShapeText();
+ cuboidview.notify('drag');
+ controller.updateModel();
+ controller.updateViewModel();
+ });
+ });
+
+ draggableFaces.forEach((face) => {
+ face.on('dragstart', () => {
+ cuboidview._flags.dragging = true;
+ ShapeView.prototype._positionateMenus.call(cuboidview);
+ cuboidview._hideShapeText();
+ cuboidview.notify('drag');
+ }).on('dragend', () => {
+ cuboidview._flags.dragging = false;
+ cuboidview._showShapeText();
+ cuboidview.notify('drag');
+ controller.updateModel();
+ controller.updateViewModel();
+ controller.updateGrabPoints();
+ });
+ });
+
+ this.makeDraggable();
+ this.makeResizable();
+ }
+
+ // computes new position of points given an initial position and a current position
+ translatePoints(startPoint, startPosition, currentPosition) {
+ const dx = currentPosition.x - startPoint.x;
+ const dy = currentPosition.y - startPoint.y;
+ const newPoints = [];
+ for (let i = 0; i < startPosition.length; i += 1) {
+ newPoints[`${i}`] = { x: startPosition[`${i}`].x + dx, y: startPosition[`${i}`].y + dy };
+ }
+ this.viewModel.setPoints(newPoints);
+ }
+
+ updateGrabPoints() {
+ // if the cuboid is front face is on the left
+ const view = this.cuboidView._uis.shape;
+ const { viewModel } = this;
+ const controller = this;
+ if (this.orientation === orientationEnum.LEFT) {
+ if (viewModel.dl.points[0].x > viewModel.fl.points[0].x) {
+ view.dorsalRightEdge.selectize({
+ points: 't,b',
+ rotationPoint: false,
+ }).resize().on('resizing', function (e) {
+ if (e.detail.event.shiftKey) {
+ controller.resizeControl(viewModel.dr,
+ this,
+ viewModel.computeSideEdgeConstraints(viewModel.dr));
+ } else {
+ const midPointUp = convertPlainArrayToActual([view.dorsalRightEdge.attr('x1'), view.dorsalRightEdge.attr('y1')])[0];
+ const midPointDown = convertPlainArrayToActual([view.dorsalRightEdge.attr('x2'), view.dorsalRightEdge.attr('y2')])[0];
+ viewModel.top.points = controller.computeHeightFace(midPointUp, 3);
+ viewModel.bot.points = controller.computeHeightFace(midPointDown, 3);
+ }
+ controller.updateViewAndVM();
+ });
+ view.drCenter.show();
+
+ view.dorsalLeftEdge.selectize(false);
+ view.dlCenter.hide();
+ this.orientation = orientationEnum.RIGHT;
+ }
+ } else if (this.orientation === orientationEnum.RIGHT) {
+ if (viewModel.dl.points[0].x <= viewModel.fl.points[0].x) {
+ view.dorsalLeftEdge.selectize({
+ points: 't,b',
+ rotationPoint: false,
+ }).resize().on('resizing', function (e) {
+ if (e.detail.event.shiftKey) {
+ controller.resizeControl(viewModel.dl,
+ this,
+ viewModel.computeSideEdgeConstraints(viewModel.dl));
+ } else {
+ const midPointUp = convertPlainArrayToActual([view.dorsalLeftEdge.attr('x1'), view.dorsalLeftEdge.attr('y1')])[0];
+ const midPointDown = convertPlainArrayToActual([view.dorsalLeftEdge.attr('x2'), view.dorsalLeftEdge.attr('y2')])[0];
+ viewModel.top.points = controller.computeHeightFace(midPointUp, 4);
+ viewModel.bot.points = controller.computeHeightFace(midPointDown, 4);
+ }
+ controller.updateViewAndVM(true);
+ });
+ view.dlCenter.show();
+
+ view.dorsalRightEdge.selectize(false);
+ view.drCenter.hide();
+ this.orientation = orientationEnum.LEFT;
+ }
+ }
+ }
+
+ makeDraggable() {
+ const controller = this;
+ const { viewModel } = this;
+ const view = this.cuboidView._uis.shape;
+ let startPoint = null;
+ let startPosition = null;
+
+ view.draggable().off('dragend').on('dragstart', (e) => {
+ startPoint = e.detail.p;
+ startPosition = viewModel.getPoints();
+ }).on('dragmove', (e) => {
+ e.preventDefault();
+ controller.translatePoints(startPoint, startPosition, e.detail.p);
+ controller.refreshView();
+ })
+ .on('dragend', () => {
+ controller.updateModel();
+ controller.updateViewModel();
+ });
+
+ // Controllable vertical edges
+ view.flCenter.draggable(function (x) {
+ const vpX = this.cx() - viewModel.vplCanvas.x > 0 ? viewModel.vplCanvas.x : 0;
+ return { x: x < viewModel.fr.canvasPoints[0].x && x > vpX + MIN_EDGE_LENGTH };
+ }).on('dragmove', function () {
+ view.frontLeftEdge.center(this.cx(), this.cy());
+
+ const position = convertPlainArrayToActual([view.frontLeftEdge.attr('x1'), view.frontLeftEdge.attr('y1')])[0];
+ const { x } = position;
+
+ const y1 = viewModel.ft.getEquation().getY(x);
+ const y2 = viewModel.fb.getEquation().getY(x);
+
+ const topPoint = { x, y: y1 };
+ const botPoint = { x, y: y2 };
+
+ viewModel.fl.points = [topPoint, botPoint];
+ controller.updateViewAndVM();
+ });
+
+ view.drCenter.draggable(function (x) {
+ let xStatus;
+ if (this.cx() < viewModel.fr.canvasPoints[0].x) {
+ xStatus = x < viewModel.fr.canvasPoints[0].x - MIN_EDGE_LENGTH
+ && x > viewModel.vprCanvas.x + MIN_EDGE_LENGTH;
+ } else {
+ xStatus = x > viewModel.fr.canvasPoints[0].x + MIN_EDGE_LENGTH
+ && x < viewModel.vprCanvas.x - MIN_EDGE_LENGTH;
+ }
+ return { x: xStatus, y: this.attr('y1') };
+ }).on('dragmove', function () {
+ view.dorsalRightEdge.center(this.cx(), this.cy());
+
+ const position = convertPlainArrayToActual([view.dorsalRightEdge.attr('x1'), view.dorsalRightEdge.attr('y1')])[0];
+ const { x } = position;
+
+ const y1 = viewModel.rt.getEquation().getY(x);
+ const y2 = viewModel.rb.getEquation().getY(x);
+
+ const topPoint = { x, y: y1 };
+ const botPoint = { x, y: y2 };
+
+ viewModel.dr.points = [topPoint, botPoint];
+ controller.updateViewAndVM();
+ });
+
+ view.dlCenter.draggable(function (x) {
+ let xStatus;
+ if (this.cx() < viewModel.fl.canvasPoints[0].x) {
+ xStatus = x < viewModel.fl.canvasPoints[0].x - MIN_EDGE_LENGTH
+ && x > viewModel.vprCanvas.x + MIN_EDGE_LENGTH;
+ } else {
+ xStatus = x > viewModel.fl.canvasPoints[0].x + MIN_EDGE_LENGTH
+ && x < viewModel.vprCanvas.x + MIN_EDGE_LENGTH;
+ }
+ return { x: xStatus, y: this.attr('y1') };
+ }).on('dragmove', function () {
+ view.dorsalLeftEdge.center(this.cx(), this.cy());
+
+ const position = convertPlainArrayToActual([view.dorsalLeftEdge.attr('x1'), view.dorsalLeftEdge.attr('y1')])[0];
+ const { x } = position;
+
+ const y1 = viewModel.lt.getEquation().getY(x);
+ const y2 = viewModel.lb.getEquation().getY(x);
+
+ const topPoint = { x, y: y1 };
+ const botPoint = { x, y: y2 };
+
+ viewModel.dl.points = [topPoint, botPoint];
+ controller.updateViewAndVM(true);
+ });
+
+ view.frCenter.draggable(function (x) {
+ return { x: x > viewModel.fl.canvasPoints[0].x, y: this.attr('y1') };
+ }).on('dragmove', function () {
+ view.frontRightEdge.center(this.cx(), this.cy());
+
+ const position = convertPlainArrayToActual([view.frontRightEdge.attr('x1'), view.frontRightEdge.attr('y1')])[0];
+ const { x } = position;
+
+ const y1 = viewModel.ft.getEquation().getY(x);
+ const y2 = viewModel.fb.getEquation().getY(x);
+
+ const topPoint = { x, y: y1 };
+ const botPoint = { x, y: y2 };
+
+ viewModel.fr.points = [topPoint, botPoint];
+ controller.updateViewAndVM(true);
+ });
+
+
+ // Controllable 'horizontal' edges
+ view.ftCenter.draggable(function (x, y) {
+ return { x: x === this.cx(), y: y < view.fbCenter.cy() - MIN_EDGE_LENGTH };
+ }).on('dragmove', function () {
+ view.frontTopEdge.center(this.cx(), this.cy());
+ controller.horizontalEdgeControl(viewModel.top, view.frontTopEdge.attr('x2'), view.frontTopEdge.attr('y2'));
+ controller.updateViewAndVM();
+ });
+
+ view.fbCenter.draggable(function (x, y) {
+ return { x: x === this.cx(), y: y > view.ftCenter.cy() + MIN_EDGE_LENGTH };
+ }).on('dragmove', function () {
+ view.frontBotEdge.center(this.cx(), this.cy());
+ controller.horizontalEdgeControl(viewModel.bot, view.frontBotEdge.attr('x2'), view.frontBotEdge.attr('y2'));
+ controller.updateViewAndVM();
+ });
+
+ // Controllable faces
+ view.left.draggable((x, y) => ({ x: x < Math.min(viewModel.dr.canvasPoints[0].x, viewModel.fr.canvasPoints[0].x) - MIN_EDGE_LENGTH, y })).on('dragmove', function () {
+ controller.faceDragControl(viewModel.left, this.attr('points'));
+ });
+ view.dorsal.draggable().on('dragmove', function () {
+ controller.faceDragControl(viewModel.dorsal, this.attr('points'));
+ });
+ view.right.draggable((x, y) => ({ x: x > Math.min(viewModel.dl.canvasPoints[0].x, viewModel.fl.canvasPoints[0].x) + MIN_EDGE_LENGTH, y })).on('dragmove', function () {
+ controller.faceDragControl(viewModel.right, this.attr('points'), true);
+ });
+ }
+
+ // Drag controls for the faces
+ faceDragControl(face, points, buildright) {
+ points = window.cvat.translate.points.canvasToActual(points);
+ points = PolylineModel.convertStringToNumberArray(points);
+ face.points = points;
+
+ this.updateViewAndVM(buildright);
+ }
+
+ // Drag controls for the non-vertical edges
+ horizontalEdgeControl(updatingFace, midX, midY) {
+ const midPoints = convertPlainArrayToActual([midX, midY])[0];
+ const leftPoints = this.updatedEdge(
+ this.viewModel.fl.points[0],
+ midPoints,
+ this.viewModel.vpl,
+ );
+ const rightPoints = this.updatedEdge(
+ this.viewModel.dr.points[0],
+ midPoints,
+ this.viewModel.vpr,
+ );
+
+ updatingFace.points = [leftPoints, midPoints, rightPoints, null];
+ }
+
+ makeResizable() {
+ const controller = this;
+ const view = this.cuboidView._uis.shape;
+ const { viewModel } = this;
+ view.frontLeftEdge.selectize({
+ points: 't,b',
+ rotationPoint: false,
+ }).resize().on('resizing', (e) => {
+ if(!e.detail.event.shiftKey){
+ const midPointUp = convertPlainArrayToActual([view.frontLeftEdge.attr('x1'), view.frontLeftEdge.attr('y1')])[0];
+ const midPointDown = convertPlainArrayToActual([view.frontLeftEdge.attr('x2'), view.frontLeftEdge.attr('y2')])[0];
+ viewModel.top.points = this.computeHeightFace(midPointUp, 1);
+ viewModel.bot.points = this.computeHeightFace(midPointDown, 1);
+ controller.updateViewAndVM();
+ }
+
+ }).on('resizestart', (e) =>{
+ if (e.detail.event.detail.event.shiftKey) {
+ showMessage('Perspective may not be adjusted on pink faces.');
+ }
+ });
+
+ view.frontRightEdge.selectize({
+ points: 't,b',
+ rotationPoint: false,
+ }).resize().on('resizing', (e) => {
+ if(!e.detail.event.shiftKey) {
+ const midPointUp = convertPlainArrayToActual([view.frontRightEdge.attr('x1'), view.frontRightEdge.attr('y1')])[0];
+ const midPointDown = convertPlainArrayToActual([view.frontRightEdge.attr('x2'), view.frontRightEdge.attr('y2')])[0];
+ viewModel.top.points = this.computeHeightFace(midPointUp, 2);
+ viewModel.bot.points = this.computeHeightFace(midPointDown, 2);
+ controller.updateViewAndVM();
+ }
+ }).on('resizestart', (e) =>{
+ if (e.detail.event.detail.event.shiftKey) {
+ showMessage('Perspective may not be adjusted on pink faces.');
+ }
+ });
+ }
+
+ computeHeightFace(point, index) {
+ switch (index) {
+ // fl
+ case 1: {
+ const p2 = this.updatedEdge(this.viewModel.fr.points[0], point, this.viewModel.vpl);
+ const p3 = this.updatedEdge(this.viewModel.dr.points[0], p2, this.viewModel.vpr);
+ const p4 = this.updatedEdge(this.viewModel.dl.points[0], point, this.viewModel.vpr);
+ return [point, p2, p3, p4];
+ }
+ // fr
+ case 2: {
+ const p2 = this.updatedEdge(this.viewModel.fl.points[0], point, this.viewModel.vpl);
+ const p3 = this.updatedEdge(this.viewModel.dr.points[0], point, this.viewModel.vpr);
+ const p4 = this.updatedEdge(this.viewModel.dl.points[0], p3, this.viewModel.vpr);
+ return [p2, point, p3, p4];
+ }
+ // dr
+ case 3: {
+ const p2 = this.updatedEdge(this.viewModel.dl.points[0], point, this.viewModel.vpl);
+ const p3 = this.updatedEdge(this.viewModel.fr.points[0], point, this.viewModel.vpr);
+ const p4 = this.updatedEdge(this.viewModel.fl.points[0], p2, this.viewModel.vpr);
+ return [p4, p3, point, p2];
+ }
+ // dl
+ case 4: {
+ const p2 = this.updatedEdge(this.viewModel.dr.points[0], point, this.viewModel.vpl);
+ const p3 = this.updatedEdge(this.viewModel.fl.points[0], point, this.viewModel.vpr);
+ const p4 = this.updatedEdge(this.viewModel.fr.points[0], p2, this.viewModel.vpr);
+ return [p3, p4, p2, point];
+ }
+ default: {
+ return [null, null, null, null];
+ }
+ }
+ }
+
+ resizeControl(vmEdge, updatedEdge, constraints) {
+ const topPoint = convertPlainArrayToActual([updatedEdge.attr('x1'), updatedEdge.attr('y1')])[0];
+ const botPoint = convertPlainArrayToActual([updatedEdge.attr('x2'), updatedEdge.attr('y2')])[0];
+
+ topPoint.y = Math.clamp(topPoint.y, constraints.y1Range.min, constraints.y1Range.max);
+ botPoint.y = Math.clamp(botPoint.y, constraints.y2Range.min, constraints.y2Range.max);
+
+ vmEdge.points = [topPoint, botPoint];
+ }
+
+ // This functions resest the perspective of the cuboid by
+ // making the top face parallel with the bottom face.
+ resetPerspective(){
+ if(this.orientation === orientationEnum.RIGHT){
+ const edgePoints = this.viewModel.dr.points;
+ const constraints = this.viewModel.computeSideEdgeConstraints(this.viewModel.dr);
+ edgePoints[0].y = constraints.y1Range.min;
+ this.viewModel.dr.points = [edgePoints[0],edgePoints[1]]
+ this.updateViewAndVM()
+ }else{
+ const edgePoints = this.viewModel.dl.points;
+ const constraints = this.viewModel.computeSideEdgeConstraints(this.viewModel.dl);
+ edgePoints[0].y = constraints.y1Range.min;
+ this.viewModel.dl.points = [edgePoints[0],edgePoints[1]]
+ this.updateViewAndVM(true)
+ }
+ }
+
+ // This method switches the pink face of the cuboid between
+ // the 2 visible faces
+ switch_orientation(){
+ function rotate( array , times ){
+ if(times>0){
+ while( times-- ){
+ var temp = array.shift();
+ array.push( temp );
+ }
+ }else{
+ while(times<0){
+ array.unshift(array.pop());
+ times++;
+ }
+ }
+ }
+ this.resetPerspective();
+
+ let top = this.viewModel.top.points;
+ let bot = this.viewModel.bot.points;
+ if(this.orientation === orientationEnum.RIGHT){
+ rotate(top,1);
+ rotate(bot, 1);
+ }else{
+ rotate(top,-1);
+ rotate(bot, -1);
+ }
+ this.viewModel.top.points = top;
+ this.viewModel.bot.points = bot;
+ this.updateViewAndVM();
+ this.updateGrabPoints();
+
+ }
+
+ // updates the view model with the actual position of the points on screen
+ // for the case where points are updated when updating the model
+ updateViewModel() {
+ let { points } = this._model._interpolatePosition(window.cvat.player.frames.current);
+ points = PolylineModel.convertStringToNumberArray(points);
+ this.viewModel.setPoints(points);
+ this.viewModel.updatePoints();
+ }
+
+ // refreshes the view and updates the viewmodel
+ updateViewAndVM(build) {
+ this.viewModel.buildBackEdge(build);
+ this.refreshView();
+ }
+
+ // given a point on an edge and a vanishing point,
+ // returns the new position of a target point
+ updatedEdge(target, base, pivot) {
+ const targetX = target.x;
+ const line = new Equation(pivot,
+ [base.x, base.y]);
+ const newY = line.getY(targetX);
+ return { x: targetX, y: newY };
+ }
+
+ // updates the model with the viewModel points
+ updateModel() {
+ const frame = window.cvat.player.frames.current;
+ const position = this._model._interpolatePosition(frame);
+
+ const viewModelpoints = this.viewModel.getPoints();
+ position.points = PolylineModel.convertNumberArrayToString(viewModelpoints);
+
+ this.updatePosition(frame, position);
+ }
+
+ refreshView() {
+ this.cuboidView._uis.shape.updateView(this.viewModel);
+ }
+
+ static removeEventsFromCube(view) {
+ const edges = view.getEdges();
+ const grabPoints = view.getGrabPoints();
+ view.off('dragmove').off('dragend').off('dragstart').off('mousedown');
+ for (let i = 0; i < edges.length; i += 1) {
+ CuboidController.removeEventsFromElement(edges[`${i}`]);
+ }
+ grabPoints.forEach((grabPoint) => {
+ CuboidController.removeEventsFromElement(grabPoint);
+ });
+
+ view.frontLeftEdge.selectize(false);
+ view.frontRightEdge.selectize(false);
+ view.dorsalRightEdge.selectize(false);
+ view.dorsalLeftEdge.selectize(false);
+
+ view.dorsal.off();
+ view.left.off();
+ view.right.off();
+ }
+
+ static removeEventsFromElement(edge) {
+ edge.off().draggable(false);
+ }
+}
+
+class CuboidModel extends PolyShapeModel {
+ constructor(data, type, cliendID, color) {
+ super(data, type, cliendID, color);
+ this._minPoints = 6;
+ this._clipToFrame = false;
+ }
+
+ static isWithinFrame(points) {
+ // Ensure at least one point is within the frame
+ const { frameWidth, frameHeight } = window.cvat.player.geometry;
+ return points.some((point) => point.x >= 0
+ && point.x <= frameWidth
+ && point.y >= 0
+ && point.y <= frameHeight);
+ }
+
+ _verifyArea(box) {
+ const withinFrame = CuboidModel.isWithinFrame([
+ { x: box.xtl, y: box.ytl },
+ { x: box.xbr, y: box.ytl },
+ { x: box.xtl, y: box.ybr },
+ { x: box.xbr, y: box.ybr },
+ ]);
+ return withinFrame && ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= AREA_TRESHOLD);
+ }
+
+ contain(mousePos, frame) {
+ function isLeft(P0, P1, P2) {
+ return ((P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y));
+ }
+
+ const pos = this._interpolatePosition(frame);
+ if (pos.outside) return false;
+ let points = PolyShapeModel.convertStringToNumberArray(pos.points);
+ points = this.makeHull(points);
+ let wn = 0;
+ for (let i = 0; i < points.length; i += 1) {
+ const p1 = points[`${i}`];
+ const p2 = points[i + 1] || points[0];
+
+ if (p1.y <= mousePos.y) {
+ if (p2.y > mousePos.y) {
+ if (isLeft(p1, p2, mousePos) > 0) {
+ wn += 1;
+ }
+ }
+ } else if (p2.y < mousePos.y) {
+ if (isLeft(p1, p2, mousePos) < 0) {
+ wn -= 1;
+ }
+ }
+ }
+
+ return wn !== 0;
+ }
+
+ makeHull(geoPoints) {
+ // Returns the convex hull, assuming that each points[i] <= points[i + 1].
+ function makeHullPresorted(points) {
+ if (points.length <= 1) return points.slice();
+
+ // Andrew's monotone chain algorithm. Positive y coordinates correspond to 'up'
+ // as per the mathematical convention, instead of 'down' as per the computer
+ // graphics convention. This doesn't affect the correctness of the result.
+
+ const upperHull = [];
+ for (let i = 0; i < points.length; i += 1) {
+ const p = points[`${i}`];
+ while (upperHull.length >= 2) {
+ const q = upperHull[upperHull.length - 1];
+ const r = upperHull[upperHull.length - 2];
+ if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
+ else break;
+ }
+ upperHull.push(p);
+ }
+ upperHull.pop();
+
+ const lowerHull = [];
+ for (let i = points.length - 1; i >= 0; i -= 1) {
+ const p = points[`${i}`];
+ while (lowerHull.length >= 2) {
+ const q = lowerHull[lowerHull.length - 1];
+ const r = lowerHull[lowerHull.length - 2];
+ if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
+ else break;
+ }
+ lowerHull.push(p);
+ }
+ lowerHull.pop();
+
+ if (upperHull.length
+ === 1 && lowerHull.length
+ === 1 && upperHull[0].x
+ === lowerHull[0].x && upperHull[0].y
+ === lowerHull[0].y) return upperHull;
+ return upperHull.concat(lowerHull);
+ }
+
+ function POINT_COMPARATOR(a, b) {
+ if (a.x < b.x) return -1;
+ if (a.x > b.x) return +1;
+ if (a.y < b.y) return -1;
+ if (a.y > b.y) return +1;
+ return 0;
+ }
+
+ const newPoints = geoPoints.slice();
+ newPoints.sort(POINT_COMPARATOR);
+ return makeHullPresorted(newPoints);
+ }
+
+ distance(mousePos, frame) {
+ const pos = this._interpolatePosition(frame);
+ if (pos.outside) return Number.MAX_SAFE_INTEGER;
+ const points = PolyShapeModel.convertStringToNumberArray(pos.points);
+ let minDistance = Number.MAX_SAFE_INTEGER;
+ for (let i = 0; i < points.length; i += 1) {
+ const p1 = points[`${i}`];
+ const p2 = points[i + 1] || points[0];
+
+ // perpendicular from point to straight length
+ const distance = (Math.abs((p2.y - p1.y) * mousePos.x
+ - (p2.x - p1.x) * mousePos.y + p2.x * p1.y - p2.y * p1.x))
+ / Math.sqrt(Math.pow(p2.y - p1.y, 2) + Math.pow(p2.x - p1.x, 2));
+
+ // check if perpendicular belongs to the straight segment
+ const a = Math.pow(p1.x - mousePos.x, 2) + Math.pow(p1.y - mousePos.y, 2);
+ const b = Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2);
+ const c = Math.pow(p2.x - mousePos.x, 2) + Math.pow(p2.y - mousePos.y, 2);
+ if (distance < minDistance && (a + b - c) >= 0 && (c + b - a) >= 0) {
+ minDistance = distance;
+ }
+ }
+ return minDistance;
+ }
+
+ resetPerspective(){
+ this.notify('perspectiveReset')
+ }
+
+ switchOrientation(){
+ this.notify('orientation')
+ }
+
+ export() {
+ const exported = PolyShapeModel.prototype.export.call(this);
+ return exported;
+ }
+
+ set draggable(value) {
+ this._draggable = value;
+ this.notify('draggable');
+ }
+
+ get draggable() {
+ return this._draggable;
+ }
+}
+
+class CuboidView extends PolyShapeView {
+ constructor(cuboidModel, cuboidController, svgContent, UIContent, textsScene) {
+ super(cuboidModel, cuboidController, svgContent, UIContent, textsScene);
+ this.model = cuboidModel;
+ cuboidController.setView(this);
+ }
+
+ // runs every time the UI is redrawn
+ _drawShapeUI(position) {
+ let { points } = position;
+ points = PolyShapeModel.convertStringToNumberArray(points);
+ const { viewModel } = this.controller();
+ viewModel.setPoints(points);
+
+ this._uis.shape = this._scenes.svg.cube(viewModel)
+ .fill(this._appearance.colors.shape).attr({
+ fill: this._appearance.fill || this._appearance.colors.shape,
+ stroke: this._appearance.stroke || this._appearance.colors.shape,
+ 'stroke-width': STROKE_WIDTH / window.cvat.player.geometry.scale,
+ // eslint-disable-next-line
+ 'z_order' : position.z_order,
+ 'fill-opacity': this._appearance.fillOpacity,
+ }).addClass('shape');
+ this._uis.shape.projectionLineEnable = this._appearance.projectionLineEnable;
+ this._controller.updateViewModel();
+ this._uis.shape.addMouseOverEvents();
+ this._uis.shape.paintOrientationLines();
+ ShapeView.prototype._drawShapeUI.call(this);
+ }
+
+ _deselect() {
+ if (this._uis.shape) {
+ PolyShapeView.prototype._deselect.call(this);
+ this._uis.shape.removeMouseOverEvents();
+ this._uis.shape.resetFaceOpacity();
+ this._uis.shape.hideProjections();
+ }
+ }
+
+ _select() {
+ if (this._uis.shape) {
+ PolyShapeView.prototype._select.call(this);
+ if (!this._controller.lock) {
+ this._uis.shape.addMouseOverEvents();
+ this._uis.shape.showProjections();
+ }
+ }
+ }
+
+ _makeEditable() {
+ if (this._uis.shape && !this._controller.lock) {
+ ShapeView.prototype._makeEditable.call(this);
+ this._uis.shape.selectize(false);
+ this._uis.shape.showGrabPoints();
+ this._controller.addEventsToCube();
+ const scaledR = POINT_RADIUS / window.cvat.player.geometry.scale;
+ const scaledPointStroke = SELECT_POINT_STROKE_WIDTH / window.cvat.player.geometry.scale;
+ $('.svg_select_points').each(function () {
+ this.instance.radius(scaledR);
+ this.instance.attr('stroke-width', scaledPointStroke);
+ });
+ }
+ }
+
+ _setupOccludedUI(occluded) {
+ if (occluded) {
+ this._uis.shape.addOccluded();
+ } else {
+ this._uis.shape.removeOccluded();
+ }
+ }
+
+ _makeNotEditable() {
+ if (this._uis.shape && this._flags.editable) {
+ CuboidController.removeEventsFromCube(this._uis.shape);
+ this._uis.shape.hideGrabPoints();
+
+ PolyShapeView.prototype._makeNotEditable.call(this);
+ }
+ }
+
+ updateColorSettings(settings) {
+ ShapeView.prototype.updateColorSettings.call(this, settings);
+ if (this._uis.shape) {
+ this._appearance.projectionLineEnable = settings['projection-lines'];
+ this.switchProjectionLine(settings['projection-lines']);
+ this._uis.shape.paintOrientationLines();
+ }
+ }
+
+ onShapeUpdate(model) {
+ ShapeView.prototype.onShapeUpdate.call(this, model);
+ if (model.updateReason === 'perspectiveReset') {
+ this._controller.resetPerspective();
+ }else if(model.updateReason === 'orientation'){
+ this._controller.switch_orientation();
+ }
+ }
+
+ updateShapeTextPosition() {
+ super.updateShapeTextPosition();
+ }
+
+ switchProjectionLine(enable) {
+ this._uis.shape.projectionLineEnable = enable;
+ }
+}
+
+// Definition of the svg object
+SVG.Cube = SVG.invent({
+ create: 'g',
+ inherit: SVG.G,
+ extend: {
+
+ constructorMethod(viewModel) {
+ this.attr('points', viewModel.getPoints());
+ this.projectionLineEnable = false;
+ this.setupFaces(viewModel);
+ this.setupEdges(viewModel);
+ this.setupProjections(viewModel);
+ this.setupGrabPoints();
+ this.hideProjections();
+ this.hideGrabPoints();
+
+ return this;
+ },
+
+ setupFaces(viewModel) {
+ this.face = this.polygon(viewModel.front.canvasPoints);
+ this.right = this.polygon(viewModel.right.canvasPoints);
+ this.dorsal = this.polygon(viewModel.dorsal.canvasPoints);
+ this.left = this.polygon(viewModel.left.canvasPoints);
+ },
+
+ setupProjections(viewModel) {
+ this.ftProj = this.line(this.updateProjectionLine(viewModel.ft.getEquation(),
+ viewModel.ft.canvasPoints[0], viewModel.vplCanvas));
+ this.fbProj = this.line(this.updateProjectionLine(viewModel.fb.getEquation(),
+ viewModel.ft.canvasPoints[0], viewModel.vplCanvas));
+ this.rtProj = this.line(this.updateProjectionLine(viewModel.rt.getEquation(),
+ viewModel.rt.canvasPoints[1], viewModel.vprCanvas));
+ this.rbProj = this.line(this.updateProjectionLine(viewModel.rb.getEquation(),
+ viewModel.rb.canvasPoints[1], viewModel.vprCanvas));
+
+ this.ftProj.stroke({ color: '#C0C0C0' });
+ this.fbProj.stroke({ color: '#C0C0C0' });
+ this.rtProj.stroke({ color: '#C0C0C0' });
+ this.rbProj.stroke({ color: '#C0C0C0' });
+ },
+
+ setupEdges(viewModel) {
+ this.frontLeftEdge = this.line(viewModel.fl.canvasPoints);
+ this.frontRightEdge = this.line(viewModel.fr.canvasPoints);
+ this.dorsalRightEdge = this.line(viewModel.dr.canvasPoints);
+ this.dorsalLeftEdge = this.line(viewModel.dl.canvasPoints);
+
+ this.frontTopEdge = this.line(viewModel.ft.canvasPoints);
+ this.rightTopEdge = this.line(viewModel.rt.canvasPoints);
+ this.frontBotEdge = this.line(viewModel.fb.canvasPoints);
+ this.rightBotEdge = this.line(viewModel.rb.canvasPoints);
+ },
+
+ setupGrabPoints() {
+ this.flCenter = this.circle().addClass('svg_select_points').addClass('svg_select_points_l');
+ this.frCenter = this.circle().addClass('svg_select_points').addClass('svg_select_points_r');
+ this.drCenter = this.circle().addClass('svg_select_points').addClass('svg_select_points_ew');
+ this.dlCenter = this.circle().addClass('svg_select_points').addClass('svg_select_points_ew');
+
+ this.ftCenter = this.circle().addClass('svg_select_points').addClass('svg_select_points_t');
+ this.fbCenter = this.circle().addClass('svg_select_points').addClass('svg_select_points_b');
+
+ const grabPoints = this.getGrabPoints();
+ const edges = this.getEdges();
+ for (let i = 0; i < grabPoints.length; i += 1) {
+ const edge = edges[`${i}`];
+ const cx = (edge.attr('x2') + edge.attr('x1')) / 2;
+ const cy = (edge.attr('y2') + edge.attr('y1')) / 2;
+ grabPoints[`${i}`].center(cx, cy);
+ }
+ },
+
+ updateGrabPoints() {
+ const centers = this.getGrabPoints();
+ const edges = this.getEdges();
+ for (let i = 0; i < centers.length; i += 1) {
+ const edge = edges[`${i}`];
+ centers[`${i}`].center(edge.cx(), edge.cy());
+ }
+ },
+
+ move(dx, dy) {
+ this.face.dmove(dx, dy);
+ this.dorsal.dmove(dx, dy);
+ this.right.dmove(dx, dy);
+ this.left.dmove(dx, dy);
+
+ const edges = this.getEdges();
+ edges.forEach((edge) => {
+ edge.dmove(dx, dy);
+ });
+ },
+
+ showProjections() {
+ if (this.projectionLineEnable) {
+ this.ftProj.show();
+ this.fbProj.show();
+ this.rtProj.show();
+ this.rbProj.show();
+ }
+ },
+
+ hideProjections() {
+ this.ftProj.hide();
+ this.fbProj.hide();
+ this.rtProj.hide();
+ this.rbProj.hide();
+ },
+
+ showGrabPoints() {
+ const grabPoints = this.getGrabPoints();
+ grabPoints.forEach((point) => {
+ point.show();
+ });
+ },
+
+ hideGrabPoints() {
+ const grabPoints = this.getGrabPoints();
+ grabPoints.forEach((point) => {
+ point.hide();
+ });
+ },
+
+ updateView(viewModel) {
+ const convertedPoints = window.cvat.translate.points.actualToCanvas(
+ viewModel.getPoints(),
+ );
+ this.updatePolygons(viewModel);
+ this.updateLines(viewModel);
+ this.updateProjections(viewModel);
+ this.updateGrabPoints();
+ this.attr('points', convertedPoints);
+ },
+
+ updatePolygons(viewModel) {
+ this.face.plot(viewModel.front.canvasPoints);
+ this.right.plot(viewModel.right.canvasPoints);
+ this.dorsal.plot(viewModel.dorsal.canvasPoints);
+ this.left.plot(viewModel.left.canvasPoints);
+ },
+
+ updateLines(viewModel) {
+ this.frontLeftEdge.plot(viewModel.fl.canvasPoints);
+ this.frontRightEdge.plot(viewModel.fr.canvasPoints);
+ this.dorsalRightEdge.plot(viewModel.dr.canvasPoints);
+ this.dorsalLeftEdge.plot(viewModel.dl.canvasPoints);
+
+ this.frontTopEdge.plot(viewModel.ft.canvasPoints);
+ this.rightTopEdge.plot(viewModel.rt.canvasPoints);
+ this.frontBotEdge.plot(viewModel.fb.canvasPoints);
+ this.rightBotEdge.plot(viewModel.rb.canvasPoints);
+ },
+
+ updateThickness() {
+ const edges = this.getEdges();
+ const width = this.attr('stroke-width');
+ const baseWidthOffset = 1.75;
+ const expandedWidthOffset = 3;
+ edges.forEach((edge) => {
+ edge.on('mouseover', function () {
+ this.attr({ 'stroke-width': width * expandedWidthOffset });
+ }).on('mouseout', function () {
+ this.attr({ 'stroke-width': width * baseWidthOffset });
+ }).stroke({ width: width * baseWidthOffset, linecap: 'round' });
+ });
+ },
+
+ updateProjections(viewModel) {
+ this.ftProj.plot(this.updateProjectionLine(viewModel.ft.getEquation(),
+ viewModel.ft.canvasPoints[0], viewModel.vplCanvas));
+ this.fbProj.plot(this.updateProjectionLine(viewModel.fb.getEquation(),
+ viewModel.ft.canvasPoints[0], viewModel.vplCanvas));
+ this.rtProj.plot(this.updateProjectionLine(viewModel.rt.getEquation(),
+ viewModel.rt.canvasPoints[1], viewModel.vprCanvas));
+ this.rbProj.plot(this.updateProjectionLine(viewModel.rb.getEquation(),
+ viewModel.rt.canvasPoints[1], viewModel.vprCanvas));
+ },
+
+ paintOrientationLines() {
+ const fillColor = this.attr('fill');
+ const selectedColor = '#ff007f';
+ this.frontTopEdge.stroke({ color: selectedColor });
+ this.frontLeftEdge.stroke({ color: selectedColor });
+ this.frontBotEdge.stroke({ color: selectedColor });
+ this.frontRightEdge.stroke({ color: selectedColor });
+
+ this.rightTopEdge.stroke({ color: fillColor });
+ this.rightBotEdge.stroke({ color: fillColor });
+ this.dorsalRightEdge.stroke({ color: fillColor });
+ this.dorsalLeftEdge.stroke({ color: fillColor });
+
+ this.face.stroke({ color: fillColor, width: 0 });
+ this.right.stroke({ color: fillColor });
+ this.dorsal.stroke({ color: fillColor });
+ this.left.stroke({ color: fillColor });
+ },
+
+ getEdges() {
+ const arr = [];
+ arr.push(this.frontLeftEdge);
+ arr.push(this.frontRightEdge);
+ arr.push(this.dorsalRightEdge);
+ arr.push(this.frontTopEdge);
+ arr.push(this.frontBotEdge);
+ arr.push(this.dorsalLeftEdge);
+ arr.push(this.rightTopEdge);
+ arr.push(this.rightBotEdge);
+ return arr;
+ },
+
+ getGrabPoints() {
+ const arr = [];
+ arr.push(this.flCenter);
+ arr.push(this.frCenter);
+ arr.push(this.drCenter);
+ arr.push(this.ftCenter);
+ arr.push(this.fbCenter);
+ arr.push(this.dlCenter);
+ return arr;
+ },
+
+ updateProjectionLine(equation, source, direction) {
+ const x1 = source.x;
+ const y1 = equation.getYCanvas(x1);
+
+ const x2 = direction.x;
+ const y2 = equation.getYCanvas(x2);
+ return [[x1, y1], [x2, y2]];
+ },
+
+ addMouseOverEvents() {
+ this._addFaceEvents();
+ },
+
+ _addFaceEvents() {
+ const group = this;
+ this.left.on('mouseover', function () {
+ this.attr({ 'fill-opacity': 0.5 });
+ }).on('mouseout', function () {
+ this.attr({ 'fill-opacity': group.attr('fill-opacity') });
+ });
+ this.dorsal.on('mouseover', function () {
+ this.attr({ 'fill-opacity': 0.5 });
+ }).on('mouseout', function () {
+ this.attr({ 'fill-opacity': group.attr('fill-opacity') });
+ });
+ this.right.on('mouseover', function () {
+ this.attr({ 'fill-opacity': 0.5 });
+ }).on('mouseout', function () {
+ this.attr({ 'fill-opacity': group.attr('fill-opacity') });
+ });
+ },
+
+ removeMouseOverEvents() {
+ const edges = this.getEdges();
+ edges.forEach((edge) => {
+ edge.off('mouseover').off('mouseout');
+ });
+ this.left.off('mouseover').off('mouseout');
+ this.dorsal.off('mouseover').off('mouseout');
+ this.right.off('mouseover').off('mouseout');
+ },
+
+ resetFaceOpacity() {
+ const group = this;
+ this.left.attr({ 'fill-opacity': group.attr('fill-opacity') });
+ this.dorsal.attr({ 'fill-opacity': group.attr('fill-opacity') });
+ this.right.attr({ 'fill-opacity': group.attr('fill-opacity') });
+ },
+
+ addOccluded() {
+ const edges = this.getEdges();
+ edges.forEach((edge) => {
+ edge.node.classList.add('occludedShape');
+ });
+ this.face.attr('stroke-width', 0);
+ this.right.attr('stroke-width', 0);
+ this.left.node.classList.add('occludedShape');
+ this.dorsal.node.classList.add('occludedShape');
+ },
+
+ removeOccluded() {
+ const edges = this.getEdges();
+ edges.forEach((edge) => {
+ edge.node.classList.remove('occludedShape');
+ });
+ this.face.attr('stroke-width', this.attr('stroke-width'));
+ this.right.attr('stroke-width', this.attr('stroke-width'));
+ this.left.node.classList.remove('occludedShape');
+ this.dorsal.node.classList.remove('occludedShape');
+ },
+ },
+ construct: {
+ cube(points) {
+ return this.put(new SVG.Cube()).constructorMethod(points);
+ },
+ },
+});
diff --git a/cvat/apps/engine/static/engine/js/shapeBuffer.js b/cvat/apps/engine/static/engine/js/shapeBuffer.js
index 3392f70d..7ea8855c 100644
--- a/cvat/apps/engine/static/engine/js/shapeBuffer.js
+++ b/cvat/apps/engine/static/engine/js/shapeBuffer.js
@@ -148,6 +148,11 @@ class ShapeBufferModel extends Listener {
let object = this._makeObject(box, polyPoints, this._shape.mode === 'interpolation');
if (object) {
+ if (this._shape.type === 'cuboid'
+ && !CuboidModel.isWithinFrame(PolyShapeModel.convertStringToNumberArray(polyPoints))) {
+ return
+ }
+
Logger.addEvent(Logger.EventType.pasteObject);
if (this._shape.type === 'box') {
this._collection.add(object, `${this._shape.mode}_${this._shape.type}`);
@@ -388,6 +393,19 @@ class ShapeBufferView {
this._shapeView = this._frameContent.polygon(points).addClass('shapeCreation').attr({
'stroke-width': STROKE_WIDTH / scale,
});
+ break;
+ case 'cuboid':
+ points = window.cvat.translate.points.canvasToActual(points);
+ points = PolylineModel.convertStringToNumberArray(points);
+ let view_model = new Cuboid2PointViewModel(points);
+ this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({
+ 'stroke-width': 0,
+ });
+
+ this._shapeViewGroup = this._frameContent.cube(view_model).addClass('shapeCreation').attr({
+ 'stroke-width': STROKE_WIDTH / scale,
+ });
+
break;
case 'polyline':
this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({
@@ -449,12 +467,14 @@ class ShapeBufferView {
let frameWidth = window.cvat.player.geometry.frameWidth;
let frameHeight = window.cvat.player.geometry.frameHeight;
- actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints);
- for (let point of actualPoints) {
- point.x = Math.clamp(point.x, 0, frameWidth);
- point.y = Math.clamp(point.y, 0, frameHeight);
+ if (this.clipToFrame) {
+ actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints);
+ for (let point of actualPoints) {
+ point.x = Math.clamp(point.x, 0, frameWidth);
+ point.y = Math.clamp(point.y, 0, frameHeight);
+ }
+ actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints);
}
- actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints);
// Set clamped points to a view in order to get an updated bounding box for a poly shape
this._shapeView.attr('points', window.cvat.translate.points.actualToCanvas(actualPoints));
diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js
index cffa804e..3454a30b 100644
--- a/cvat/apps/engine/static/engine/js/shapeCollection.js
+++ b/cvat/apps/engine/static/engine/js/shapeCollection.js
@@ -422,6 +422,7 @@ class ShapeCollectionModel extends Listener {
switch (shape.model.type.split('_')[1]) {
case 'box':
case 'polygon':
+ case 'cuboid':
if (shape.model.contain(pos, this._frame)) {
let distance = shape.model.distance(pos, this._frame);
if (distance < closedShape.minDistance) {
@@ -573,6 +574,10 @@ class ShapeCollectionModel extends Listener {
annotation: 0,
interpolation: 0,
},
+ cuboids: {
+ annotation: 0,
+ interpolation: 0,
+ },
manually: 0,
interpolated: 0,
total: 0,
@@ -596,6 +601,10 @@ class ShapeCollectionModel extends Listener {
annotation: 0,
interpolation: 0,
},
+ cuboids: {
+ annotation: 0,
+ interpolation: 0,
+ },
manually: 0,
interpolated: 0,
total: 0,
@@ -620,6 +629,9 @@ class ShapeCollectionModel extends Listener {
case 'points':
statistic[statShape.labelId].points[statShape.mode] ++;
break;
+ case 'cuboid':
+ statistic[statShape.labelId].cuboids[statShape.mode]++;
+ break;
default:
throw Error(`Unknown shape type found: ${statShape.type}`);
}
@@ -634,6 +646,9 @@ class ShapeCollectionModel extends Listener {
totalForLabels.polylines.interpolation += statistic[labelId].polylines.interpolation;
totalForLabels.points.annotation += statistic[labelId].points.annotation;
totalForLabels.points.interpolation += statistic[labelId].points.interpolation;
+ totalForLabels.cuboids.annotation += statistic[labelId].cuboids.annotation;
+ totalForLabels.cuboids.interpolation += statistic[labelId].cuboids.interpolation;
+ totalForLabels.cuboids.interpolation += statistic[labelId].cuboids.interpolation;
totalForLabels.manually += statistic[labelId].manually;
totalForLabels.interpolated += statistic[labelId].interpolated;
totalForLabels.total += statistic[labelId].total;
@@ -1081,6 +1096,20 @@ class ShapeCollectionController {
}
}
+ resetPerspectiveFromActiveShape(){
+ let activeShape = this._model.activeShape;
+ if (activeShape && activeShape instanceof CuboidModel) {
+ this.activeShape.resetPerspective();
+ }
+ }
+
+ switchOrientationFromActiveShape(){
+ let activeShape = this._model.activeShape;
+ if (activeShape && activeShape instanceof CuboidModel) {
+ this.activeShape.switchOrientation();
+ }
+ }
+
removePointFromActiveShape(idx) {
this._model.removePointFromActiveShape(idx);
}
@@ -1136,6 +1165,7 @@ class ShapeCollectionView {
this._colorByLabelRadio = $('#colorByLabelRadio');
this._colorByGroupCheckbox = $('#colorByGroupCheckbox');
this._filterView = new FilterView(this._controller.filterController);
+ this._enabledProjectionCheckbox = $('#projectionLineEnable')
this._currentViews = [];
this._currentModels = [];
@@ -1145,7 +1175,8 @@ class ShapeCollectionView {
this._scale = 1;
this._rotation = 0;
this._colorSettings = {
- "fill-opacity": 0
+ 'fill-opacity': 0,
+ 'projection-lines':false,
};
this._showAllInterpolationBox.on('change', (e) => {
@@ -1216,6 +1247,13 @@ class ShapeCollectionView {
this._colorSettings['colors-by-label'] = this._controller.colorsByGroup.bind(this._controller);
+ for (const view of this._currentViews) {
+ view.updateColorSettings(this._colorSettings);
+ }
+ });
+
+ this._enabledProjectionCheckbox.on('change', e => {
+ this._colorSettings['projection-lines'] = e.target.checked;
for (let view of this._currentViews) {
view.updateColorSettings(this._colorSettings);
}
@@ -1287,7 +1325,14 @@ class ShapeCollectionView {
case "drag_polygon":
this._controller.switchDraggableForActive();
break;
+ case "reset_perspective":
+ this._controller.resetPerspectiveFromActiveShape();
+ break;
+ case "switch_orientation":
+ this._controller.switchOrientationFromActiveShape();
+ break;
}
+
});
let shortkeys = window.cvat.config.shortkeys;
diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js
index d9da2ba2..7748d295 100644
--- a/cvat/apps/engine/static/engine/js/shapeCreator.js
+++ b/cvat/apps/engine/static/engine/js/shapeCreator.js
@@ -17,6 +17,8 @@
STROKE_WIDTH:false
SVG:false
BorderSticker: false
+ CuboidModel:false
+ Cuboid2PointViewModel:false
*/
class ShapeCreatorModel extends Listener {
@@ -54,7 +56,7 @@ class ShapeCreatorModel extends Listener {
}
// FIXME: In the future we have to make some generic solution
- if (this._defaultMode === 'interpolation'
+ if (this._defaultMode === 'interpolation'
&& ['box', 'points', 'box_by_4_points'].includes(this._defaultType)) {
data.shapes = [];
data.shapes.push(Object.assign({}, result, data));
@@ -126,7 +128,7 @@ class ShapeCreatorModel extends Listener {
}
set defaultType(type) {
- if (!['box', 'box_by_4_points', 'points', 'polygon', 'polyline'].includes(type)) {
+ if (!['box', 'box_by_4_points', 'points', 'polygon', 'polyline', "cuboid"].includes(type)) {
throw Error(`Unknown shape type found ${type}`);
}
this._defaultType = type;
@@ -235,7 +237,7 @@ class ShapeCreatorView {
// FIXME: In the future we have to make some generic solution
const mode = this._modeSelector.prop('value');
const type = $(e.target).prop('value');
- if (type !== 'box' && type !== 'box_by_4_points'
+ if (type !== 'box' && type !== 'box_by_4_points'
&& !(type === 'points' && this._polyShapeSize === 1) && mode !== 'annotation') {
this._modeSelector.prop('value', 'annotation');
this._controller.setDefaultShapeMode('annotation');
@@ -313,6 +315,59 @@ class ShapeCreatorView {
});
}
+ _createCuboidEvent() {
+ let sizeUI = null;
+ const backFaceOffset = 20;
+ this._drawInstance = this._frameContent.rect().draw({ snapToGrid: 0.1 }).addClass("shapeCreation").attr({
+ "stroke-width": STROKE_WIDTH / this._scale,
+ })
+ .on("drawstop", (e) => {
+ if (this._cancel) {
+ return;
+ }
+ if (sizeUI) {
+ sizeUI.rm();
+ sizeUI = null;
+ }
+
+ const rect = window.cvat.translate.box.canvasToActual(e.target.getBBox());
+
+ const p1 = { x: rect.x, y: rect.y + 1 };
+ const p2 = { x: rect.x, y: rect.y - 1 + rect.height };
+ const p3 = { x: rect.x + rect.width, y: rect.y };
+ const p4 = { x: rect.x + rect.width, y: rect.y + rect.height };
+
+ const p5 = { x: p3.x + backFaceOffset, y: p3.y - backFaceOffset + 1 };
+ const p6 = { x: p3.x + backFaceOffset, y: p4.y - backFaceOffset - 1 };
+
+ let points = [p1, p2, p3, p4, p5, p6];
+
+ if (!CuboidModel.isWithinFrame(points)) {
+ this._controller.switchCreateMode(true);
+ return;
+ }
+
+ const viewModel = new Cuboid2PointViewModel(points);
+ points = viewModel.getPoints();
+
+ points = PolyShapeModel.convertNumberArrayToString(points);
+ e.target.setAttribute("points",
+ window.cvat.translate.points.actualToCanvas(points));
+ this._controller.finish({ points }, this._type);
+ this._controller.switchCreateMode(true);
+ })
+ .on("drawupdate", (e) => {
+ sizeUI = drawBoxSize.call(sizeUI, this._frameContent,
+ this._frameText, e.target.getBBox());
+ })
+ .on("drawcancel", () => {
+ if (sizeUI) {
+ sizeUI.rm();
+ sizeUI = null;
+ }
+ });
+ }
+
_createPolyEvents() {
// If number of points for poly shape specified, use it.
// Dicrement number on draw new point events. Drawstart trigger when create first point
@@ -369,7 +424,10 @@ class ShapeCreatorView {
x: e.detail.event.clientX,
y: e.detail.event.clientY,
};
- numberOfPoints ++;
+ numberOfPoints += 1;
+ if (this._type === "cuboid" && numberOfPoints === 4) {
+ this._drawInstance.draw("done");
+ }
});
this._commonBordersCheckbox.css('display', '').trigger('change.shapeCreator');
@@ -426,8 +484,8 @@ class ShapeCreatorView {
}
});
// Also we need callback on drawdone event for get points
- this._drawInstance.on('drawdone', function(e) {
- let actualPoints = window.cvat.translate.points.canvasToActual(e.target.getAttribute('points'));
+ this._drawInstance.on("drawdone", (e) => {
+ let actualPoints = window.cvat.translate.points.canvasToActual(e.target.getAttribute("points"));
actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints);
// Min 2 points for polyline and 3 points for polygon
@@ -437,32 +495,218 @@ class ShapeCreatorView {
}
else if (this._type === 'polygon' && actualPoints.length < 3) {
showMessage("Min 3 points must be for polygon drawing.");
- }
- else {
- let frameWidth = window.cvat.player.geometry.frameWidth;
- let frameHeight = window.cvat.player.geometry.frameHeight;
- for (let point of actualPoints) {
+ } else if (this._type === "cuboid" && (actualPoints.length !== 4)) {
+ showMessage("Exactly 4 points must be used for cuboid drawing."
+ + " Second point must be below the first point."
+ + "(HINT) The first 3 points define the front face"
+ + " and the last point should define the depth and orientation of the cuboid ");
+ } else if (this._type === "cuboid") {
+ let points = this._makeCuboid(actualPoints);
+ const viewModel = new Cuboid2PointViewModel(points);
+ if (!CuboidModel.isWithinFrame(points)) {
+ this._controller.switchCreateMode(true);
+ return;
+ }
+
+ points = viewModel.getPoints();
+
+ points = PolyShapeModel.convertNumberArrayToString(points);
+ e.target.setAttribute("points",
+ window.cvat.translate.points.actualToCanvas(points));
+ this._controller.finish({ points }, this._type);
+ this._controller.switchCreateMode(true);
+ } else {
+ const { frameWidth } = window.cvat.player.geometry;
+ const { frameHeight } = window.cvat.player.geometry;
+ for (const point of actualPoints) {
point.x = Math.clamp(point.x, 0, frameWidth);
point.y = Math.clamp(point.y, 0, frameHeight);
}
- actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints);
+ actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints);
// Update points in a view in order to get an updated box
- e.target.setAttribute('points', window.cvat.translate.points.actualToCanvas(actualPoints));
- let polybox = e.target.getBBox();
- let w = polybox.width;
- let h = polybox.height;
- let area = w * h;
- let type = this._type;
-
- if (area >= AREA_TRESHOLD || type === 'points' && numberOfPoints || type === 'polyline' && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) {
- this._controller.finish({points: actualPoints}, type);
+ e.target.setAttribute("points", window.cvat.translate.points.actualToCanvas(actualPoints));
+ const polybox = e.target.getBBox();
+ const w = polybox.width;
+ const h = polybox.height;
+ const area = w * h;
+ const type = this._type;
+
+ if (area >= AREA_TRESHOLD || type === "points" && numberOfPoints || type === "polyline" && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) {
+ this._controller.finish({ points: actualPoints }, type);
}
}
}
this._controller.switchCreateMode(true);
- }.bind(this));
+ });
+ }
+
+ _sortClockwise(points){
+ points.sort((a, b) => a.y - b.y);
+ // Get center y
+ const cy = (points[0].y + points[points.length - 1].y) / 2;
+
+ // Sort from right to left
+ points.sort((a, b) => b.x - a.x);
+
+ // Get center x
+ const cx = (points[0].x + points[points.length - 1].x) / 2;
+
+ // Center point
+ var center = {
+ x : cx,
+ y : cy
+ };
+
+ // Starting angle used to reference other angles
+ var startAng;
+ points.forEach((point) => {
+ var ang = Math.atan2(point.y - center.y, point.x - center.x);
+ if (!startAng) {
+ startAng = ang;
+ } else {
+ if (ang < startAng) { // ensure that all points are clockwise of the start point
+ ang += Math.PI * 2;
+ }
+ }
+ point.angle = ang; // add the angle to the point
+ });
+
+ // first sort clockwise
+ points.sort((a, b) => a.angle - b.angle);
+ return points.reverse();
+ }
+
+ _makeCuboid(actualPoints){
+ let unsortedPlanePoints = actualPoints.slice(0,3);
+ function rotate( array , times ){
+ while( times-- ){
+ var temp = array.shift();
+ array.push( temp );
+ }
+ }
+
+ let plane1;
+ let plane2 = {p1:actualPoints[0], p2:actualPoints[0], p3:actualPoints[0], p4:actualPoints[0]};
+
+ // completing the plane
+ const vector = {
+ x: actualPoints[2].x - actualPoints[1].x,
+ y: actualPoints[2].y - actualPoints[1].y,
+ }
+
+ // sorting the first plane
+ unsortedPlanePoints.push({x:actualPoints[0].x + vector.x, y: actualPoints[0].y + vector.y});
+ let sortedPlanePoints = this._sortClockwise(unsortedPlanePoints);
+ let leftIndex = 0;
+ for(let i = 0; i<4; i++){
+ leftIndex = sortedPlanePoints[`${i}`].x < sortedPlanePoints[`${leftIndex}`].x ? i : leftIndex;
+ }
+ rotate(sortedPlanePoints,leftIndex);
+ plane1 = {
+ p1:sortedPlanePoints[0],
+ p2:sortedPlanePoints[1],
+ p3:sortedPlanePoints[2],
+ p4:sortedPlanePoints[3]
+ };
+
+ const vec = {
+ x: actualPoints[3].x - actualPoints[2].x,
+ y: actualPoints[3].y - actualPoints[2].y,
+ };
+ // determine the orientation
+ let angle = Math.atan2(vec.y,vec.x);
+
+ // making the other plane
+ plane2.p1 = {x:plane1.p1.x + vec.x, y:plane1.p1.y + vec.y};
+ plane2.p2 = {x:plane1.p2.x + vec.x, y:plane1.p2.y + vec.y};
+ plane2.p3 = {x:plane1.p3.x + vec.x, y:plane1.p3.y + vec.y};
+ plane2.p4 = {x:plane1.p4.x + vec.x, y:plane1.p4.y + vec.y};
+
+
+ let points ;
+ // right
+ if(Math.abs(angle) < Math.PI/2-0.1){
+ return this._setupCuboidPoints(actualPoints);
+ }
+
+ // left
+ else if(Math.abs(angle) > Math.PI/2+0.1){
+ return this._setupCuboidPoints(actualPoints);
+ }
+ // down
+ else if(angle>0){
+ points = [plane1.p1,plane2.p1,plane1.p2,plane2.p2,plane1.p3,plane2.p3];
+ points[0].y+=0.1;
+ points[4].y+=0.1;
+ return [plane1.p1,plane2.p1,plane1.p2,plane2.p2,plane1.p3,plane2.p3];
+ }
+ // up
+ else{
+ points = [plane2.p1,plane1.p1,plane2.p2,plane1.p2,plane2.p3,plane1.p3];
+ points[0].y+=0.1;
+ points[4].y+=0.1;
+ return points;
+ }
+ }
+
+ _setupCuboidPoints(actualPoints) {
+ let left,right,left2,right2;
+ let p1,p2,p3,p4,p5,p6;
+
+ const height = Math.abs(actualPoints[0].x - actualPoints[1].x)
+ < Math.abs(actualPoints[1].x - actualPoints[2].x)
+ ? Math.abs(actualPoints[1].y - actualPoints[0].y)
+ : Math.abs(actualPoints[1].y - actualPoints[2].y);
+
+ // seperate into left and right point
+ // we pick the first and third point because we know assume they will be on
+ // opposite corners
+ if(actualPoints[0].x < actualPoints[2].x){
+ left = actualPoints[0];
+ right = actualPoints[2];
+ }else{
+ left = actualPoints[2];
+ right = actualPoints[0];
+ }
+
+ // get other 2 points using the given height
+ if(left.y < right.y){
+ left2 = { x: left.x, y: left.y + height };
+ right2 = { x: right.x, y: right.y - height };
+ }else{
+ left2 = { x: left.x, y: left.y - height };
+ right2 = { x: right.x, y: right.y + height };
+ }
+
+ // get the vector for the last point relative to the previous point
+ const vec = {
+ x: actualPoints[3].x - actualPoints[2].x,
+ y: actualPoints[3].y - actualPoints[2].y,
+ };
+
+ if(left.y < left2.y){
+ p1 = left;
+ p2 = left2;
+ }else{
+ p1 = left2;
+ p2 = left;
+ }
+
+ if(right.y < right2.y){
+ p3 = right;
+ p4 = right2;
+ }else{
+ p3 = right2;
+ p4 = right;
+ }
+
+ p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
+ p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
+
+ p1.y += 0.1;
+ return [p1, p2, p3, p4, p5, p6];
}
_create() {
@@ -608,15 +852,21 @@ class ShapeCreatorView {
});
this._createPolyEvents();
break;
+ case "cuboid":
+ this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 }).addClass("shapeCreation").attr({
+ "stroke-width": STROKE_WIDTH / this._scale,
+ });
+ this._createPolyEvents();
+ break;
default:
throw Error(`Bad type found ${this._type}`);
}
}
_rescaleDrawPoints() {
- let scale = this._scale;
- $('.svg_draw_point').each(function() {
- this.instance.radius(2.5 / scale).attr('stroke-width', 1 / scale);
+ const scale = this._scale;
+ $(".svg_draw_point").each(function () {
+ this.instance.radius(2.5 / scale).attr("stroke-width", 1 / scale);
});
}
diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js
index 1212b92b..0ddf3522 100644
--- a/cvat/apps/engine/static/engine/js/shapes.js
+++ b/cvat/apps/engine/static/engine/js/shapes.js
@@ -50,6 +50,7 @@ class ShapeModel extends Listener {
this._hiddenShape = false;
this._hiddenText = true;
this._updateReason = null;
+ this._clipToFrame = true;
this._importAttributes(data.attributes, positions);
}
@@ -608,6 +609,10 @@ class ShapeModel extends Listener {
get selected() {
return this._selected;
}
+
+ get clipToFrame() {
+ return this._clipToFrame;
+ }
}
@@ -931,8 +936,10 @@ class PolyShapeModel extends ShapeModel {
let points = PolyShapeModel.convertStringToNumberArray(position.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);
+ if (this.clipToFrame) {
+ point.x = Math.clamp(point.x, 0, window.cvat.player.geometry.frameWidth);
+ point.y = Math.clamp(point.y, 0, window.cvat.player.geometry.frameHeight);
+ }
box.xtl = Math.min(box.xtl, point.x);
box.ytl = Math.min(box.ytl, point.y);
@@ -1673,6 +1680,16 @@ class ShapeView extends Listener {
dragPolyItem.addClass('hidden');
}
+ let resetPerpectiveItem = this._shapeContextMenu.find('.cuboidItem[action="reset_perspective"]');
+ let switchOrientationItem = this._shapeContextMenu.find('.cuboidItem[action="switch_orientation"]');
+ if(type[1] === 'cuboid'){
+ resetPerpectiveItem.removeClass('hidden');
+ switchOrientationItem.removeClass('hidden');
+ }else{
+ resetPerpectiveItem.addClass('hidden');
+ switchOrientationItem.addClass('hidden');
+ }
+
this._shapeContextMenu.finish().show(100);
let x = Math.min(e.pageX, this._rightBorderFrame - this._shapeContextMenu[0].scrollWidth);
let y = Math.min(e.pageY, this._bottomBorderFrame - this._shapeContextMenu[0].scrollHeight);
@@ -3327,25 +3344,30 @@ function buildShapeModel(data, type, clientID, color) {
case 'interpolation_polygon':
case 'annotation_polygon':
return new PolygonModel(data, type, clientID, color);
+ case 'interpolation_cuboid':
+ case 'annotation_cuboid':
+ return new CuboidModel(data, type, clientID, color);
}
throw Error('Unreacheable code was reached.');
}
-
function buildShapeController(shapeModel) {
switch (shapeModel.type) {
- case 'interpolation_box':
- case 'annotation_box':
- return new BoxController(shapeModel);
- case 'interpolation_points':
- case 'annotation_points':
- return new PointsController(shapeModel);
- case 'interpolation_polyline':
- case 'annotation_polyline':
- return new PolylineController(shapeModel);
- case 'interpolation_polygon':
- case 'annotation_polygon':
- return new PolygonController(shapeModel);
+ case 'interpolation_box':
+ case 'annotation_box':
+ return new BoxController(shapeModel);
+ case 'interpolation_points':
+ case 'annotation_points':
+ return new PointsController(shapeModel);
+ case 'interpolation_polyline':
+ case 'annotation_polyline':
+ return new PolylineController(shapeModel);
+ case 'interpolation_polygon':
+ case 'annotation_polygon':
+ return new PolygonController(shapeModel);
+ case 'interpolation_cuboid':
+ case 'annotation_cuboid':
+ return new CuboidController(shapeModel);
}
throw Error('Unreacheable code was reached.');
}
@@ -3353,18 +3375,21 @@ function buildShapeController(shapeModel) {
function buildShapeView(shapeModel, shapeController, svgContent, UIContent, textsContent) {
switch (shapeModel.type) {
- case 'interpolation_box':
- case 'annotation_box':
- return new BoxView(shapeModel, shapeController, svgContent, UIContent, textsContent);
- case 'interpolation_points':
- case 'annotation_points':
- return new PointsView(shapeModel, shapeController, svgContent, UIContent, textsContent);
- case 'interpolation_polyline':
- case 'annotation_polyline':
- return new PolylineView(shapeModel, shapeController, svgContent, UIContent, textsContent);
- case 'interpolation_polygon':
- case 'annotation_polygon':
- return new PolygonView(shapeModel, shapeController, svgContent, UIContent, textsContent);
+ case 'interpolation_box':
+ case 'annotation_box':
+ return new BoxView(shapeModel, shapeController, svgContent, UIContent, textsContent);
+ case 'interpolation_points':
+ case 'annotation_points':
+ return new PointsView(shapeModel, shapeController, svgContent, UIContent, textsContent);
+ case 'interpolation_polyline':
+ case 'annotation_polyline':
+ return new PolylineView(shapeModel, shapeController, svgContent, UIContent, textsContent);
+ case 'interpolation_polygon':
+ case 'annotation_polygon':
+ return new PolygonView(shapeModel, shapeController, svgContent, UIContent, textsContent);
+ case 'interpolation_cuboid':
+ case 'annotation_cuboid':
+ return new CuboidView(shapeModel, shapeController, svgContent, UIContent, textsContent);
}
throw Error('Unreacheable code was reached.');
}
diff --git a/cvat/apps/engine/static/engine/js/utils.js b/cvat/apps/engine/static/engine/js/utils.js
new file mode 100644
index 00000000..ad54d840
--- /dev/null
+++ b/cvat/apps/engine/static/engine/js/utils.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 Intel Corporation
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/* exported convertPlainArrayToActual, convertToArray, intersection */
+
+/* global
+ PolylineModel:false
+*/
+
+// Takes a 2d array of canvas points and transforms it to an array of point objects
+function convertPlainArrayToActual(arr) {
+ let actual = [{ x: arr[0], y: arr[1] }];
+ actual = PolylineModel.convertNumberArrayToString(actual);
+ actual = window.cvat.translate.points.canvasToActual(actual);
+ actual = PolylineModel.convertStringToNumberArray(actual);
+ return actual;
+}
+
+// converts an array of point objects to a 2D array
+function convertToArray(points) {
+ const arr = [];
+ points.forEach((point) => {
+ arr.push([point.x, point.y]);
+ });
+ return arr;
+}
+
+
+function line(p1, p2) {
+ const a = p1[1] - p2[1];
+ const b = p2[0] - p1[0];
+ const c = b * p1[1] + a * p1[0];
+ return [a, b, c];
+}
+
+function intersection(p1, p2, p3, p4) {
+ const L1 = line(p1, p2);
+ const L2 = line(p3, p4);
+
+ const D = L1[0] * L2[1] - L1[1] * L2[0];
+ const Dx = L1[2] * L2[1] - L1[1] * L2[2];
+ const Dy = L1[0] * L2[2] - L1[2] * L2[0];
+
+ let x = null;
+ let y = null;
+ if (D !== 0) {
+ x = Dx / D;
+ y = Dy / D;
+ return [x, y];
+ }
+
+ return null;
+}
diff --git a/cvat/apps/engine/static/engine/stylesheet.css b/cvat/apps/engine/static/engine/stylesheet.css
index 6e0d7b65..bc6e771d 100644
--- a/cvat/apps/engine/static/engine/stylesheet.css
+++ b/cvat/apps/engine/static/engine/stylesheet.css
@@ -224,6 +224,14 @@
cursor: w-resize;
}
+.svg_select_points_ns{
+ cursor: ns-resize;
+}
+
+.svg_select_points_ew{
+ cursor: ew-resize;
+}
+
.shape-creator-border-point {
opacity: 0.55;
}
diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html
index 70feb5c4..edfcfd18 100644
--- a/cvat/apps/engine/templates/engine/annotation.html
+++ b/cvat/apps/engine/templates/engine/annotation.html
@@ -34,6 +34,7 @@
{% block head_js_cvat %}
{{ block.super }}
+
@@ -53,6 +54,7 @@
+
@@ -96,6 +98,9 @@
Switch Lock
Split
Enable Dragging
+ Reset Perspective
+ Switch Perspective Orientation
+