Skip to content

Commit

Permalink
React UI: Sidebar with objects and optimizations for annotation view (c…
Browse files Browse the repository at this point in the history
…vat-ai#1089)

* Basic layout for objects panel

* Objects header

* A little name refactoring

* Side panel base layout

* Firefox specific exceptions

* Some minor fixes

* React & canvas optimizations

* Icons refactoring

* Little style refactoring

* Some style fixes

* Improved side panel with objects

* Actual attribute values

* Actual icons

* Hidden > visible

* hidden -> __internal

* Fixed hidden in ui

* Fixed some issues in canvas

* Fixed list height

* Color picker for labels

* A bit fixed design

* Actual header icons

* Changing attributes and switchable buttons

* Removed react memo (will reoptimize better)

* Sorting methods, removed cache from cvat-core (a lot of bugs related with it)

* Label switchers

* Fixed bug with update timestamp for shapes

* Annotation state refactoring

* Removed old resetCache calls

* Optimized top & left panels. Number of renders significantly decreased

* Optimized some extra renders

* Accelerated performance

* Fixed two minor issues

* Canvas improvements

* Minor fixes

* Removed extra code
  • Loading branch information
bsekachev authored and Chris Lee-Messer committed Mar 5, 2020
1 parent 8fea4bc commit db09055
Show file tree
Hide file tree
Showing 101 changed files with 4,532 additions and 1,424 deletions.
5 changes: 4 additions & 1 deletion cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
angle: number;
canvasSize: Size;
image: string;
imageID: number | null;
imageOffset: number;
imageSize: Size;
focusData: FocusData;
Expand Down Expand Up @@ -183,6 +184,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: 0,
},
image: '',
imageID: null,
imageOffset: 0,
imageSize: {
height: 0,
Expand Down Expand Up @@ -300,9 +302,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: (frameData.width as number),
};

if (!this.data.rememberAngle) {
if (this.data.imageID !== frameData.number && !this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageID = frameData.number;

this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
Expand Down
206 changes: 149 additions & 57 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ export interface CanvasView {
html(): HTMLDivElement;
}

interface ShapeDict {
[index: number]: SVG.Shape;
}

interface TextDict {
[index: number]: SVG.Text;
}

function darker(color: string, percentage: number): string {
const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100));
const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100));
Expand All @@ -78,8 +70,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
private gridPath: SVGPathElement;
private gridPattern: SVGPatternElement;
private controller: CanvasController;
private svgShapes: ShapeDict;
private svgTexts: TextDict;
private svgShapes: Record<number, SVG.Shape>;
private svgTexts: Record<number, SVG.Text>;
private drawnStates: Record<number, any>;
private geometry: Geometry;
private drawHandler: DrawHandler;
private editHandler: EditHandler;
Expand Down Expand Up @@ -382,6 +375,38 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}

private setupObjects(states: any[]): void {
this.deactivate();

const created = [];
const updated = [];
for (const state of states) {
if (!(state.clientID in this.drawnStates)) {
created.push(state);
} else {
const drawnState = this.drawnStates[state.clientID];
if (drawnState.updated !== state.updated || drawnState.frame !== state.frame) {
updated.push(state);
}
}
}
const newIDs = states.map((state: any): number => state.clientID);
const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID)
.filter((id: number): boolean => !newIDs.includes(id))
.map((id: number): any => this.drawnStates[id]);
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
}

this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}

this.addObjects(created);
this.updateObjects(updated);
}

private selectize(value: boolean, shape: SVG.Element): void {
const self = this;

Expand Down Expand Up @@ -457,6 +482,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.geometry = controller.geometry;
this.svgShapes = {};
this.svgTexts = {};
this.drawnStates = {};
this.activeElement = null;
this.mode = Mode.IDLE;

Expand Down Expand Up @@ -621,31 +647,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
}

public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
function setupObjects(objects: any[]): void {
const ctm = this.content.getScreenCTM()
.inverse().multiply(this.background.getScreenCTM());

this.deactivate();

// TODO: Compute difference

// Instead of simple clearing let's remove all objects properly
for (const id of Object.keys(this.svgShapes)) {
if (id in this.svgTexts) {
this.svgTexts[id].remove();
}

this.svgShapes[id].remove();
}

this.svgTexts = {};
this.svgShapes = {};

this.addObjects(ctm, objects);
// TODO: Update objects
// TODO: Delete objects
}

this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE_CHANGED) {
if (!model.image.length) {
Expand All @@ -658,6 +659,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.transformCanvas();
}
} else if (reason === UpdateReasons.FITTED_CANVAS) {
// Canvas geometry is going to be changed. Old object positions aren't valid any more
this.setupObjects([]);
this.moveCanvas();
this.resizeCanvas();
} else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) {
Expand All @@ -669,7 +672,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects();
}
setupObjects.call(this, this.controller.objects);
this.setupObjects(this.controller.objects);
if (this.mode === Mode.MERGE) {
this.mergeHandler.repeatSelection();
}
Expand Down Expand Up @@ -792,20 +795,94 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.canvas;
}

private addObjects(ctm: SVGMatrix, states: any[]): void {
private saveState(state: any): void {
this.drawnStates[state.clientID] = {
clientID: state.clientID,
outside: state.outside,
occluded: state.occluded,
hidden: state.hidden,
lock: state.lock,
points: [...state.points],
attributes: { ...state.attributes },
};
}

private updateObjects(states: any[]): void {
for (const state of states) {
const { clientID } = state;
const drawnState = this.drawnStates[clientID];

if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
const none = state.hidden || state.outside;
this.svgShapes[clientID].style('display', none ? 'none' : '');
}

if (drawnState.occluded !== state.occluded) {
if (state.occluded) {
this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded');
} else {
this.svgShapes[clientID].removeClass('cvat_canvas_shape_occluded');
}
}

if (drawnState.points
.some((p: number, id: number): boolean => p !== state.points[id])
) {
const translatedPoints: number[] = translateBetweenSVG(
this.background, this.content, state.points,
);

if (state.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translatedPoints;

this.svgShapes[clientID].attr({
x: xtl,
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
});
} else {
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc}${val} `;
}

return `${acc}${val},`;
}, '',
);

this.svgShapes[clientID].attr('points', stringified);
}
}

for (const attrID of Object.keys(state.attributes)) {
if (state.attributes[attrID] !== drawnState.attributes[attrID]) {
const text = this.svgTexts[state.clientID];
if (text) {
const [span] = this.svgTexts[state.clientID].node
.querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[];
if (span && span.textContent) {
const prefix = span.textContent.split(':').slice(0, -1).join(':');
span.textContent = `${prefix}: ${state.attributes[attrID]}`;
}
}
}
}

this.saveState(state);
}
}

private addObjects(states: any[]): void {
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state);
} else {
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = [];
for (let i = 0; i <= points.length - 1; i += 2) {
let point: SVGPoint = this.background.createSVGPoint();
point.x = points[i];
point.y = points[i + 1];
point = point.matrixTransform(ctm);
translatedPoints.push(point.x, point.y);
}
const translatedPoints: number[] = translateBetweenSVG(
this.background, this.content, points,
);

// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
Expand Down Expand Up @@ -833,16 +910,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addPoints(stringified, state);
}
}

// TODO: Use enums after typification cvat-core
if (state.visibility === 'all') {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
}

this.saveState(state);
}
}

Expand All @@ -852,17 +922,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
const shape = this.svgShapes[this.activeElement.state.clientID];
shape.removeClass('cvat_canvas_shape_activated');

(shape as any).off('dragstart');
(shape as any).off('dragend');
(shape as any).draggable(false);

if (state.shapeType !== 'points') {
this.selectize(false, shape);
}

(shape as any).off('resizestart');
(shape as any).off('resizing');
(shape as any).off('resizedone');
(shape as any).resize(false);

// Hide text only if it is hidden by settings
// TODO: Hide text only if it is hidden by settings
const text = this.svgTexts[state.clientID];
if (text && state.visibility === 'shape') {
if (text) {
text.remove();
delete this.svgTexts[state.clientID];
}
Expand Down Expand Up @@ -893,7 +968,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shape.addClass('cvat_canvas_shape_activated');
let text = this.svgTexts[activeElement.clientID];
// Draw text if it's hidden by default
if (!text && state.visibility === 'shape') {
if (!text) {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
this.updateTextPosition(
Expand Down Expand Up @@ -1057,6 +1132,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
rect.addClass('cvat_canvas_shape_occluded');
}

if (state.hidden || state.outside) {
rect.style('display', 'none');
}

return rect;
}

Expand All @@ -1076,6 +1155,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
polygon.addClass('cvat_canvas_shape_occluded');
}

if (state.hidden || state.outside) {
polygon.style('display', 'none');
}

return polygon;
}

Expand All @@ -1095,6 +1178,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
polyline.addClass('cvat_canvas_shape_occluded');
}

if (state.hidden || state.outside) {
polyline.style('display', 'none');
}

return polyline;
}

Expand All @@ -1120,6 +1207,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}).style({
'fill-opacity': 1,
});

if (state.hidden || state.outside) {
group.style('display', 'none');
}

group.bbox = shape.bbox.bind(shape);
group.clone = shape.clone.bind(shape);

Expand Down
13 changes: 1 addition & 12 deletions cvat-core/src/annotations-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,9 @@
const {
ObjectShape,
ObjectType,
colors,
} = require('./enums');
const ObjectState = require('./object-state');
const colors = [
'#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66',
'#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33',
'#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37',
'#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380'];

function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData;
Expand Down Expand Up @@ -381,9 +377,6 @@
// Remove other shapes
for (const object of objectsForMerge) {
object.removed = true;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
}
}

Expand Down Expand Up @@ -470,7 +463,6 @@

// Remove source object
object.removed = true;
object.resetCache();
}

group(objectStates, reset) {
Expand All @@ -490,9 +482,6 @@
const groupIdx = reset ? 0 : ++this.groups.max;
for (const object of objectsForGroup) {
object.group = groupIdx;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
}

return groupIdx;
Expand Down
Loading

0 comments on commit db09055

Please sign in to comment.