diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ff9f40..ec84bfb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Special behaviour for attribute value ``__undefined__`` (invisibility, no shortcuts to be set in AAM) - Dialog window with some helpful information about using filters +- Ability to display a bitmap in the new UI - Added option to display shape text always ### Changed diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index ea6bfa0e..c3e7b640 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -104,6 +104,7 @@ Canvas itself handles: select(objectState: any): void; fitCanvas(): void; + bitmap(enabled: boolean): void; dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; @@ -196,4 +197,5 @@ Standard JS events are used. | zoomCanvas() | + | - | - | - | - | - | - | + | | cancel() | - | + | + | + | + | + | + | + | | configure() | + | - | - | - | - | - | - | - | +| bitmap() | + | + | + | + | + | + | + | + | | setZLayer() | + | + | + | + | + | + | + | + | diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index d46edb0e..fab01d0b 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -152,6 +152,16 @@ polyline.cvat_canvas_shape_splitting { box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); } +#cvat_canvas_bitmap { + pointer-events: none; + position: absolute; + z-index: 4; + background: black; + width: 100%; + height: 100%; + box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); +} + #cvat_canvas_grid { position: absolute; z-index: 2; diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index c601034f..1df3efe2 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -50,6 +50,7 @@ interface Canvas { select(objectState: any): void; fitCanvas(): void; + bitmap(enable: boolean): void; dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; @@ -88,6 +89,10 @@ class CanvasImpl implements Canvas { ); } + public bitmap(enable: boolean): void { + this.model.bitmap(enable); + } + public dragCanvas(enable: boolean): void { this.model.dragCanvas(enable); } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 888b5499..13f99be1 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -103,8 +103,9 @@ export enum UpdateReasons { GROUP = 'group', SELECT = 'select', CANCEL = 'cancel', + BITMAP = 'bitmap', DRAG_CANVAS = 'drag_canvas', - ZOOM_CANVAS = 'ZOOM_CANVAS', + ZOOM_CANVAS = 'zoom_canvas', CONFIG_UPDATED = 'config_updated', } @@ -122,6 +123,7 @@ export enum Mode { } export interface CanvasModel { + readonly imageBitmap: boolean; readonly image: Image | null; readonly objects: any[]; readonly zLayer: number | null; @@ -155,6 +157,7 @@ export interface CanvasModel { select(objectState: any): void; fitCanvas(width: number, height: number): void; + bitmap(enabled: boolean): void; dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; @@ -168,6 +171,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { angle: number; canvasSize: Size; configuration: Configuration; + imageBitmap: boolean; image: Image | null; imageID: number | null; imageOffset: number; @@ -204,6 +208,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { displayAllText: false, undefinedAttrValue: '', }, + imageBitmap: false, image: null, imageID: null, imageOffset: 0, @@ -290,6 +295,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.notify(UpdateReasons.OBJECTS_UPDATED); } + public bitmap(enabled: boolean): void { + this.data.imageBitmap = enabled; + this.notify(UpdateReasons.BITMAP); + } + public dragCanvas(enable: boolean): void { if (enable && this.data.mode !== Mode.IDLE) { throw Error(`Canvas is busy. Action: ${this.data.mode}`); @@ -555,6 +565,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { return this.data.zLayer; } + public get imageBitmap(): boolean { + return this.data.imageBitmap; + } + public get image(): Image | null { return this.data.image; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index bd460be6..da226cf4 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -48,6 +48,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private text: SVGSVGElement; private adoptedText: SVG.Container; private background: HTMLCanvasElement; + private bitmap: HTMLCanvasElement; private grid: SVGSVGElement; private content: SVGSVGElement; private adoptedContent: SVG.Container; @@ -287,7 +288,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } private moveCanvas(): void { - for (const obj of [this.background, this.grid]) { + for (const obj of [this.background, this.grid, this.bitmap]) { obj.style.top = `${this.geometry.top}px`; obj.style.left = `${this.geometry.left}px`; } @@ -305,7 +306,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private transformCanvas(): void { // Transform canvas - for (const obj of [this.background, this.grid, this.content]) { + for (const obj of [this.background, this.grid, this.content, this.bitmap]) { obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`; } @@ -360,7 +361,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } private resizeCanvas(): void { - for (const obj of [this.background, this.grid]) { + for (const obj of [this.background, this.grid, this.bitmap]) { obj.style.width = `${this.geometry.image.width}px`; obj.style.height = `${this.geometry.image.height}px`; } @@ -549,6 +550,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container); this.background = window.document.createElement('canvas'); + this.bitmap = window.document.createElement('canvas'); // window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); @@ -593,6 +595,8 @@ export class CanvasViewImpl implements CanvasView, Listener { this.text.setAttribute('id', 'cvat_canvas_text_content'); this.background.setAttribute('id', 'cvat_canvas_background'); this.content.setAttribute('id', 'cvat_canvas_content'); + this.bitmap.setAttribute('id', 'cvat_canvas_bitmap'); + this.bitmap.style.display = 'none'; // Setup wrappers this.canvas.setAttribute('id', 'cvat_canvas_wrapper'); @@ -608,6 +612,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.appendChild(this.loadingAnimation); this.canvas.appendChild(this.text); this.canvas.appendChild(this.background); + this.canvas.appendChild(this.bitmap); this.canvas.appendChild(this.grid); this.canvas.appendChild(this.content); @@ -709,6 +714,14 @@ export class CanvasViewImpl implements CanvasView, Listener { this.configuration = model.configuration; this.setupObjects([]); this.setupObjects(model.objects); + } else if (reason === UpdateReasons.BITMAP) { + const { imageBitmap } = model; + if (imageBitmap) { + this.bitmap.style.display = ''; + this.redrawBitmap(); + } else { + this.bitmap.style.display = 'none'; + } } else if (reason === UpdateReasons.IMAGE_CHANGED) { const { image } = model; if (!image) { @@ -882,12 +895,59 @@ export class CanvasViewImpl implements CanvasView, Listener { this.mode = Mode.IDLE; this.canvas.style.cursor = ''; } + + if (model.imageBitmap + && [UpdateReasons.IMAGE_CHANGED, + UpdateReasons.OBJECTS_UPDATED, + UpdateReasons.SET_Z_LAYER, + ].includes(reason) + ) { + this.redrawBitmap(); + } } public html(): HTMLDivElement { return this.canvas; } + private redrawBitmap(): void { + const width = +this.background.style.width.slice(0, -2); + const height = +this.background.style.height.slice(0, -2); + this.bitmap.setAttribute('width', `${width}px`); + this.bitmap.setAttribute('height', `${height}px`); + const states = this.controller.objects; + + const ctx = this.bitmap.getContext('2d'); + if (ctx) { + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, width, height); + for (const state of states) { + if (state.hidden || state.outside) continue; + ctx.fillStyle = 'white'; + if (['rectangle', 'polygon'].includes(state.shapeType)) { + const points = state.shapeType === 'rectangle' ? [ + state.points[0], // xtl + state.points[1], // ytl + state.points[2], // xbr + state.points[1], // ytl + state.points[2], // xbr + state.points[3], // ybr + state.points[0], // xtl + state.points[3], // ybr + ] : state.points; + ctx.beginPath(); + ctx.moveTo(points[0], points[1]); + for (let i = 0; i < points.length; i += 2) { + ctx.lineTo(points[i], points[i + 1]); + } + ctx.closePath(); + } + + ctx.fill(); + } + } + } + private saveState(state: any): void { this.drawnStates[state.clientID] = { clientID: state.clientID, diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 03b56cc2..f06b0fb0 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -18,6 +18,7 @@ export enum SettingsActionTypes { CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY', CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY', CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS', + CHANGE_SHOW_UNLABELED_REGIONS = 'CHANGE_SHOW_UNLABELED_REGIONS', CHANGE_FRAME_STEP = 'CHANGE_FRAME_STEP', CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED', SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM', @@ -67,6 +68,15 @@ export function changeShapesBlackBorders(blackBorders: boolean): AnyAction { }; } +export function changeShowBitmap(showBitmap: boolean): AnyAction { + return { + type: SettingsActionTypes.CHANGE_SHOW_UNLABELED_REGIONS, + payload: { + showBitmap, + }, + }; +} + export function switchRotateAll(rotateAll: boolean): AnyAction { return { type: SettingsActionTypes.SWITCH_ROTATE_ALL, diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 4f38f3db..8296f287 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -43,6 +43,7 @@ interface Props { colorBy: ColorBy; selectedOpacity: number; blackBorders: boolean; + showBitmap: boolean; grid: boolean; gridSize: number; gridColor: GridColor; @@ -120,6 +121,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { colorBy, selectedOpacity, blackBorders, + showBitmap, frameData, frameAngle, annotations, @@ -214,6 +216,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { this.updateShapesView(); } + if (prevProps.showBitmap !== showBitmap) { + canvasInstance.bitmap(showBitmap); + } + if (prevProps.frameAngle !== frameAngle) { canvasInstance.rotate(frameAngle); } @@ -573,6 +579,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { for (const state of annotations) { let shapeColor = ''; + if (colorBy === ColorBy.INSTANCE) { shapeColor = state.color; } else if (colorBy === ColorBy.GROUP) { @@ -588,6 +595,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { if (handler && handler.nested) { handler.nested.fill({ color: shapeColor }); } + (shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 }); (shapeView as any).instance.stroke({ color: blackBorders ? 'black' : shapeColor }); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx index 56c5b5ac..d3ed0f03 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx @@ -24,12 +24,14 @@ interface Props { opacity: number; selectedOpacity: number; blackBorders: boolean; + showBitmap: boolean; collapseAppearance(): void; changeShapesColorBy(event: RadioChangeEvent): void; changeShapesOpacity(event: SliderValue): void; changeSelectedShapesOpacity(event: SliderValue): void; changeShapesBlackBorders(event: CheckboxChangeEvent): void; + changeShowBitmap(event: CheckboxChangeEvent): void; } function AppearanceBlock(props: Props): JSX.Element { @@ -39,11 +41,13 @@ function AppearanceBlock(props: Props): JSX.Element { opacity, selectedOpacity, blackBorders, + showBitmap, collapseAppearance, changeShapesColorBy, changeShapesOpacity, changeSelectedShapesOpacity, changeShapesBlackBorders, + changeShowBitmap, } = props; return ( @@ -85,6 +89,12 @@ function AppearanceBlock(props: Props): JSX.Element { > Black borders + + Show bitmap + diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index 1a8cd5bd..b173d870 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -29,6 +29,7 @@ interface Props { opacity: number; selectedOpacity: number; blackBorders: boolean; + showBitmap: boolean; collapseSidebar(): void; collapseAppearance(): void; @@ -37,6 +38,7 @@ interface Props { changeShapesOpacity(event: SliderValue): void; changeSelectedShapesOpacity(event: SliderValue): void; changeShapesBlackBorders(event: CheckboxChangeEvent): void; + changeShowBitmap(event: CheckboxChangeEvent): void; } function ObjectsSideBar(props: Props): JSX.Element { @@ -47,12 +49,14 @@ function ObjectsSideBar(props: Props): JSX.Element { opacity, selectedOpacity, blackBorders, + showBitmap, collapseSidebar, collapseAppearance, changeShapesColorBy, changeShapesOpacity, changeSelectedShapesOpacity, changeShapesBlackBorders, + changeShowBitmap, } = props; const appearanceProps = { @@ -62,11 +66,13 @@ function ObjectsSideBar(props: Props): JSX.Element { opacity, selectedOpacity, blackBorders, + showBitmap, changeShapesColorBy, changeShapesOpacity, changeSelectedShapesOpacity, changeShapesBlackBorders, + changeShowBitmap, }; return ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss index 07f3c0f6..51ca31fd 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss @@ -23,6 +23,11 @@ background: $background-color-2; border-bottom: none; height: 230px; + + > .ant-collapse-content-box { + padding: 10px; + } + } } } @@ -254,6 +259,10 @@ width: 33%; } } + + .ant-checkbox-wrapper { + margin-left: 0px; + } } .cvat-object-item-menu { diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index fc4d62e5..12501c44 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -62,6 +62,7 @@ interface StateToProps { colorBy: ColorBy; selectedOpacity: number; blackBorders: boolean; + showBitmap: boolean; grid: boolean; gridSize: number; gridColor: GridColor; @@ -171,6 +172,7 @@ function mapStateToProps(state: CombinedState): StateToProps { colorBy, selectedOpacity, blackBorders, + showBitmap, }, }, shortcuts: { @@ -194,6 +196,7 @@ function mapStateToProps(state: CombinedState): StateToProps { colorBy, selectedOpacity, blackBorders, + showBitmap, grid, gridSize, gridColor, diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index bb6162e9..309700cc 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -27,6 +27,7 @@ import { changeShapesOpacity as changeShapesOpacityAction, changeSelectedShapesOpacity as changeSelectedShapesOpacityAction, changeShapesBlackBorders as changeShapesBlackBordersAction, + changeShowBitmap as changeShowUnlabeledRegionsAction, } from 'actions/settings-actions'; @@ -37,6 +38,7 @@ interface StateToProps { opacity: number; selectedOpacity: number; blackBorders: boolean; + showBitmap: boolean; } interface DispatchToProps { @@ -47,6 +49,7 @@ interface DispatchToProps { changeShapesOpacity(shapesOpacity: number): void; changeSelectedShapesOpacity(selectedShapesOpacity: number): void; changeShapesBlackBorders(blackBorders: boolean): void; + changeShowBitmap(showBitmap: boolean): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -61,6 +64,7 @@ function mapStateToProps(state: CombinedState): StateToProps { opacity, selectedOpacity, blackBorders, + showBitmap, }, }, } = state; @@ -72,6 +76,7 @@ function mapStateToProps(state: CombinedState): StateToProps { opacity, selectedOpacity, blackBorders, + showBitmap, }; } @@ -132,6 +137,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { changeShapesBlackBorders(blackBorders: boolean): void { dispatch(changeShapesBlackBordersAction(blackBorders)); }, + changeShowBitmap(showBitmap: boolean) { + dispatch(changeShowUnlabeledRegionsAction(showBitmap)); + }, }; } @@ -177,6 +185,11 @@ class ObjectsSideBarContainer extends React.PureComponent { changeShapesBlackBorders(event.target.checked); }; + private changeShowBitmap = (event: CheckboxChangeEvent): void => { + const { changeShowBitmap } = this.props; + changeShowBitmap(event.target.checked); + }; + public render(): JSX.Element { const { sidebarCollapsed, @@ -185,6 +198,7 @@ class ObjectsSideBarContainer extends React.PureComponent { opacity, selectedOpacity, blackBorders, + showBitmap, collapseSidebar, collapseAppearance, } = this.props; @@ -197,12 +211,14 @@ class ObjectsSideBarContainer extends React.PureComponent { opacity={opacity} selectedOpacity={selectedOpacity} blackBorders={blackBorders} + showBitmap={showBitmap} collapseSidebar={collapseSidebar} collapseAppearance={collapseAppearance} changeShapesColorBy={this.changeShapesColorBy} changeShapesOpacity={this.changeShapesOpacity} changeSelectedShapesOpacity={this.changeSelectedShapesOpacity} changeShapesBlackBorders={this.changeShapesBlackBorders} + changeShowBitmap={this.changeShowBitmap} /> ); } diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 2ab26f7c..d794ffd7 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -435,6 +435,7 @@ export interface ShapesSettingsState { opacity: number; selectedOpacity: number; blackBorders: boolean; + showBitmap: boolean; } export interface SettingsState { diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 632ed5b9..00cc5527 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -22,6 +22,7 @@ const defaultState: SettingsState = { opacity: 3, selectedOpacity: 30, blackBorders: false, + showBitmap: false, }, workspace: { autoSave: false, @@ -128,6 +129,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case SettingsActionTypes.CHANGE_SHOW_UNLABELED_REGIONS: { + return { + ...state, + shapes: { + ...state.shapes, + showBitmap: action.payload.showBitmap, + }, + }; + } case SettingsActionTypes.CHANGE_FRAME_STEP: { return { ...state, @@ -227,18 +237,18 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case BoundariesActionTypes.RESET_AFTER_ERROR: case AnnotationActionTypes.GET_JOB_SUCCESS: { const { job } = action.payload; return { - ...state, + ...defaultState, player: { - ...state.player, + ...defaultState.player, resetZoom: job && job.task.mode === 'annotation', }, }; } - case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; }