Cuboid annotation (#678)

* Cuboid feature

* migration files

* Refactored cuboidShape
Fixed a bug where coloring by label would not update cuboids properly
Fixed a bug where the select points would not scale properly on initialization

* Removed math.js dependency
Implemented custom line intersection function

* new cvat formatting with labelled points

* Added MIT License to js files that were missing it

* Added simple constraints to the cuboids

* reverted commit for settings for vscode to hide local path

* fixed locking for cuboids

* fixed cuboid View when locked

* fixed occlusion view for cuboids

* Allow cuboid points to be outside the frame dimensions.

Signed-off-by: Tritin Truong <truongtritin98@gmail.com>

* Added stricter constraints on cuboid edges.

* Slightly stricter restrictions for edge case

* Cleaned up unused imports

* removed dashed lines on cuboids

* Moved projection lines to settings tab

* Fixed Cuboid shape buffer \

* Fix migrations (two 022 migrations after merge with the develop branch).

* Fix compatibility issues with auto segmentation.

* Grab points and update control scheme

* Greatly improved control scheme, fixed shape merging
Fixed Cuboid upload

* Fixed slight visual bug when dragging faces

* Some optimizations

* Hiding the grab point on creation
Small refactoring

* Fixed some cases where cuboid breaks

* Fixed upload for videos

* Removed perspective effects

* Made left back edge editable

* left back edge resizable

* fix statistics bug

* added toggles for the back edges

* Constraints for the back edges

* Fix creation bug

* Tightened creation constraints

* Fixing the code style

* updated message for invalid cuboids

* Code style

* More style fixes

* Codacy fixes

* added shift control for edges

* More Codacy fixes

* More Codacy fixes

* Double arrows for cursor

* Fix Drag bug

* More Codacy fixes

* Fix double quotes

* Fix camel case

* More camelcase fixes

* Generic object sink fixes

* Various codacy fixes

* Codacy

* Double quotes

* Fix migrations

* Updated shape creation
Fix jittering

* Adjusted constraints

* Codacy fixes

* Codacy fixes again

* Drawing cuboids from the top and bottom

* Codacy

* Resetting perspective on cuboids

* Choosing orientation of cuboids.

* Codacy fix

* Merge cleanup

* revert vs-code settings

* Update settings.json

Co-authored-by: timbowl <54648082+timbowl@users.noreply.github.com>
Co-authored-by: Nikita Manovich <40690625+nmanovic@users.noreply.github.com>
main
Tritin Truong 6 years ago committed by GitHub
parent ca2f2164ac
commit 0bb92f27c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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(',')))

@ -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;

@ -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):

@ -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,
)

@ -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({

@ -1,3 +1,9 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* exported buildAnnotationSaver */
/* global

@ -295,6 +295,8 @@ function setupMenu(job, task, shapeCollectionModel,
<td> ${byLabelsStat[labelId].polylines.interpolation} </td>
<td> ${byLabelsStat[labelId].points.annotation} </td>
<td> ${byLabelsStat[labelId].points.interpolation} </td>
<td> ${byLabelsStat[labelId].cuboids.annotation} </td>
<td> ${byLabelsStat[labelId].cuboids.interpolation} </td>
<td> ${byLabelsStat[labelId].manually} </td>
<td> ${byLabelsStat[labelId].interpolated} </td>
<td class="semiBold"> ${byLabelsStat[labelId].total} </td>
@ -312,6 +314,8 @@ function setupMenu(job, task, shapeCollectionModel,
<td> ${totalStat.polylines.interpolation} </td>
<td> ${totalStat.points.annotation} </td>
<td> ${totalStat.points.interpolation} </td>
<td> ${totalStat.cuboids.annotation} </td>
<td> ${totalStat.cuboids.interpolation} </td>
<td> ${totalStat.manually} </td>
<td> ${totalStat.interpolated} </td>
<td> ${totalStat.total} </td>
@ -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();

File diff suppressed because it is too large Load Diff

@ -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));

@ -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;

@ -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);
});
}

@ -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.');
}

@ -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;
}

@ -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;
}

@ -34,6 +34,7 @@
{% block head_js_cvat %}
{{ block.super }}
<script type="text/javascript" src="{% static 'engine/js/utils.js' %}"></script>
<script type="text/javascript">
window.UI_URL = "{{ ui_url }}";
</script>
@ -53,6 +54,7 @@
<script type="text/javascript" src="{% static 'engine/js/bootstrap.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/shapes.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/cuboidShape.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/shapeCollection.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/player.js' %}"></script>
@ -96,6 +98,9 @@
<li action="switch_lock"> Switch Lock </li>
<li class="interpolationItem" action="split_track"> Split </li>
<li class="polygonItem" action="drag_polygon"> Enable Dragging </li>
<li class="cuboidItem" action="reset_perspective"> Reset Perspective </li>
<li class="cuboidItem" action="switch_orientation"> Switch Perspective Orientation </li>
</ul>
<ul id="playerContextMenu" class='custom-menu' oncontextmenu="return false;">
@ -329,6 +334,10 @@
<td> <label> Rotate All Images </label> </td>
<td> <input type = "checkbox" id="rotateAllImages" class="settingsBox"/> </td>
</tr>
<tr>
<td> <label> Cuboid Projection Lines: </label> </td>
<td><input type="checkbox" id="projectionLineEnable" class="settingsBox"/></td>
</tr>
</table>
</div>
@ -395,6 +404,7 @@
<td colspan="2"> Polygons </td>
<td colspan="2"> Polylines </td>
<td colspan="2"> Points </td>
<td colspan="2"> Cuboids </td>
<td> Manually </td>
<td> Interpolated </td>
<td> Total </td>
@ -409,6 +419,8 @@
<td> T </td>
<td> S </td>
<td> T </td>
<td> S </td>
<td> T </td>
<td> </td> <!-- Empty -->
<td> </td> <!-- Empty -->
</tr>
@ -441,6 +453,7 @@
<option value="polygon" class="regular"> Polygon </option>
<option value="polyline" class="regular"> Polyline </option>
<option value="points" class="regular"> Points </option>
<option value="cuboid" class="regular"> Cuboid </option>
</select>
<div id="polyShapeSizeWrapper">
<label for="polyShapeSize" class="regular h2"> Poly Shape Size: </label>

Loading…
Cancel
Save