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
  • +