Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React UI: Sidebar with objects and optimizations for annotation view #1089

Merged
merged 37 commits into from
Jan 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cef6364
Basic layout for objects panel
bsekachev Jan 17, 2020
35c6546
Objects header
bsekachev Jan 17, 2020
86d1dc4
Merge branch 'develop' into bs/cvat_ui
bsekachev Jan 17, 2020
faf1597
A little name refactoring
bsekachev Jan 17, 2020
d48ec96
Side panel base layout
bsekachev Jan 18, 2020
198e0de
Firefox specific exceptions
bsekachev Jan 18, 2020
d2d0393
Some minor fixes
bsekachev Jan 18, 2020
16749a8
React & canvas optimizations
bsekachev Jan 20, 2020
bd93a38
Icons refactoring
bsekachev Jan 20, 2020
60a225b
Little style refactoring
bsekachev Jan 20, 2020
36ba082
Some style fixes
bsekachev Jan 20, 2020
ece8ad1
Improved side panel with objects
bsekachev Jan 21, 2020
88c6d38
Actual attribute values
bsekachev Jan 21, 2020
c42a63d
Actual icons
bsekachev Jan 21, 2020
9abdc3a
Hidden > visible
bsekachev Jan 21, 2020
eaa0d0c
hidden -> __internal
bsekachev Jan 21, 2020
783f265
Fixed hidden in ui
bsekachev Jan 21, 2020
378a024
Fixed some issues in canvas
bsekachev Jan 21, 2020
877f495
Fixed list height
bsekachev Jan 21, 2020
85a6174
Color picker for labels
bsekachev Jan 21, 2020
3c8fda1
A bit fixed design
bsekachev Jan 21, 2020
b25d4bc
Actual header icons
bsekachev Jan 21, 2020
24ad806
Changing attributes and switchable buttons
bsekachev Jan 21, 2020
738bb97
Removed react memo (will reoptimize better)
bsekachev Jan 21, 2020
8f07e09
Merge branch 'develop' into bs/cvat_ui
bsekachev Jan 22, 2020
5f8270f
Sorting methods, removed cache from cvat-core (a lot of bugs related …
bsekachev Jan 22, 2020
ed8c3bc
Label switchers
bsekachev Jan 22, 2020
47dc482
Fixed bug with update timestamp for shapes
bsekachev Jan 22, 2020
bbdd1a5
Annotation state refactoring
bsekachev Jan 22, 2020
9de9e26
Removed old resetCache calls
bsekachev Jan 22, 2020
89c9117
Optimized top & left panels. Number of renders significantly decreased
bsekachev Jan 23, 2020
543fa38
Optimized some extra renders
bsekachev Jan 23, 2020
34220ec
Accelerated performance
bsekachev Jan 24, 2020
7c1481c
Fixed two minor issues
bsekachev Jan 24, 2020
7745659
Canvas improvements
bsekachev Jan 24, 2020
20a7ad4
Minor fixes
bsekachev Jan 24, 2020
84d053b
Removed extra code
bsekachev Jan 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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