React UI: Improved rotation feature (#1206)

Co-authored-by: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
main
Dmitry Kalinin 6 years ago committed by GitHub
parent da69a40b96
commit adb66b57ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -32,11 +32,6 @@ Canvas itself handles:
### API Methods ### API Methods
```ts ```ts
enum Rotation {
ANTICLOCKWISE90,
CLOCKWISE90,
}
enum RectDrawingMethod { enum RectDrawingMethod {
CLASSIC = 'By 2 points', CLASSIC = 'By 2 points',
EXTREME_POINTS = 'By 4 points' EXTREME_POINTS = 'By 4 points'
@ -79,7 +74,7 @@ Canvas itself handles:
setZLayer(zLayer: number | null): void; setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(frameAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -147,7 +142,7 @@ Standard JS events are used.
canvas.fitCanvas(); canvas.fitCanvas();
// Next you can use its API methods. For example: // Next you can use its API methods. For example:
canvas.rotate(window.Canvas.Rotation.CLOCKWISE90); canvas.rotate(270);
canvas.draw({ canvas.draw({
enabled: true, enabled: true,
shapeType: 'rectangle', shapeType: 'rectangle',

@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { import {
Rotation,
DrawData, DrawData,
MergeData, MergeData,
SplitData, SplitData,
@ -37,7 +36,7 @@ interface Canvas {
setZLayer(zLayer: number | null): void; setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number | null, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -97,8 +96,8 @@ class CanvasImpl implements Canvas {
this.model.activate(clientID, attributeID); this.model.activate(clientID, attributeID);
} }
public rotate(rotation: Rotation, remember: boolean = false): void { public rotate(rotationAngle: number): void {
this.model.rotate(rotation, remember); this.model.rotate(rotationAngle);
} }
public focus(clientID: number, padding: number = 0): void { public focus(clientID: number, padding: number = 0): void {
@ -140,7 +139,6 @@ class CanvasImpl implements Canvas {
export { export {
CanvasImpl as Canvas, CanvasImpl as Canvas,
Rotation,
CanvasVersion, CanvasVersion,
RectDrawingMethod, RectDrawingMethod,
}; };

@ -72,11 +72,6 @@ export enum FrameZoom {
MAX = 10, MAX = 10,
} }
export enum Rotation {
ANTICLOCKWISE90,
CLOCKWISE90,
}
export enum UpdateReasons { export enum UpdateReasons {
IMAGE_CHANGED = 'image_changed', IMAGE_CHANGED = 'image_changed',
IMAGE_ZOOMED = 'image_zoomed', IMAGE_ZOOMED = 'image_zoomed',
@ -135,7 +130,7 @@ export interface CanvasModel {
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number | null, attributeID: number | null): void; activate(clientID: number | null, attributeID: number | null): void;
rotate(rotation: Rotation, remember: boolean): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void; focus(clientID: number, padding: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -166,7 +161,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
gridSize: Size; gridSize: Size;
left: number; left: number;
objects: any[]; objects: any[];
rememberAngle: boolean;
scale: number; scale: number;
top: number; top: number;
zLayer: number | null; zLayer: number | null;
@ -208,7 +202,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}, },
left: 0, left: 0,
objects: [], objects: [],
rememberAngle: false,
scale: 1, scale: 1,
top: 0, top: 0,
zLayer: null, zLayer: null,
@ -323,10 +316,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return; return;
} }
if (!this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageSize = { this.data.imageSize = {
height: (frameData.height as number), height: (frameData.height as number),
width: (frameData.width as number), width: (frameData.width as number),
@ -355,16 +344,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.SHAPE_ACTIVATED); this.notify(UpdateReasons.SHAPE_ACTIVATED);
} }
public rotate(rotation: Rotation, remember: boolean = false): void { public rotate(rotationAngle: number): void {
if (rotation === Rotation.CLOCKWISE90) { if (this.data.angle !== rotationAngle) {
this.data.angle += 90; this.data.angle = (360 + Math.floor((rotationAngle) / 90) * 90) % 360;
} else { this.fit();
this.data.angle -= 90;
} }
this.data.angle %= 360;
this.data.rememberAngle = remember;
this.fit();
} }
public focus(clientID: number, padding: number): void { public focus(clientID: number, padding: number): void {

@ -17,6 +17,7 @@ import {
ObjectType, ObjectType,
Task, Task,
FrameSpeed, FrameSpeed,
Rotation,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
@ -129,6 +130,7 @@ export enum AnnotationActionTypes {
CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS', CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS',
FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS', FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS',
FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED', FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED',
ROTATE_FRAME = 'ROTATE_FRAME',
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER', SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER',
} }
@ -666,6 +668,27 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
export function rotateCurrentFrame(rotation: Rotation): AnyAction {
const state: CombinedState = getStore().getState();
const { number: frameNumber } = state.annotation.player.frame;
const { startFrame } = state.annotation.job.instance;
const { frameAngles } = state.annotation.player;
const { rotateAll } = state.settings.player;
const frameAngle = (frameAngles[frameNumber - startFrame]
+ (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360;
return {
type: AnnotationActionTypes.ROTATE_FRAME,
payload: {
offset: frameNumber - state.annotation.job.instance.startFrame,
angle: frameAngle,
rotateAll,
},
};
}
export function dragCanvas(enabled: boolean): AnyAction { export function dragCanvas(enabled: boolean): AnyAction {
return { return {
type: AnnotationActionTypes.DRAG_CANVAS, type: AnnotationActionTypes.DRAG_CANVAS,

@ -37,6 +37,7 @@ interface Props {
selectedStatesID: number[]; selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frameAngle: number;
frame: number; frame: number;
opacity: number; opacity: number;
colorBy: ColorBy; colorBy: ColorBy;
@ -101,6 +102,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
selectedOpacity, selectedOpacity,
blackBorders, blackBorders,
frameData, frameData,
frameAngle,
annotations, annotations,
canvasInstance, canvasInstance,
sidebarCollapsed, sidebarCollapsed,
@ -149,6 +151,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.setZLayer(curZLayer); canvasInstance.setZLayer(curZLayer);
} }
if (prevProps.frameAngle !== frameAngle) {
canvasInstance.rotate(frameAngle);
}
this.activateOnCanvas(); this.activateOnCanvas();
} }
@ -305,11 +311,13 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { const {
annotations, annotations,
frameData, frameData,
frameAngle,
canvasInstance, canvasInstance,
} = this.props; } = this.props;
if (frameData !== null) { if (frameData !== null) {
canvasInstance.setup(frameData, annotations); canvasInstance.setup(frameData, annotations);
canvasInstance.rotate(frameAngle);
} }
} }

@ -12,6 +12,7 @@ import {
import { import {
ActiveControl, ActiveControl,
Rotation
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
@ -22,9 +23,9 @@ import {
Canvas, Canvas,
} from 'cvat-canvas'; } from 'cvat-canvas';
import RotateControl from './rotate-control';
import CursorControl from './cursor-control'; import CursorControl from './cursor-control';
import MoveControl from './move-control'; import MoveControl from './move-control';
import RotateControl from './rotate-control';
import FitControl from './fit-control'; import FitControl from './fit-control';
import ResizeControl from './resize-control'; import ResizeControl from './resize-control';
import DrawRectangleControl from './draw-rectangle-control'; import DrawRectangleControl from './draw-rectangle-control';
@ -37,23 +38,23 @@ import SplitControl from './split-control';
interface Props { interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
rotateAll: boolean;
activeControl: ActiveControl; activeControl: ActiveControl;
mergeObjects(enabled: boolean): void; mergeObjects(enabled: boolean): void;
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void; splitTrack(enabled: boolean): void;
rotateFrame(rotation: Rotation): void;
} }
export default function ControlsSideBarComponent(props: Props): JSX.Element { export default function ControlsSideBarComponent(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
activeControl, activeControl,
rotateAll,
mergeObjects, mergeObjects,
groupObjects, groupObjects,
splitTrack, splitTrack,
rotateFrame,
} = props; } = props;
return ( return (
@ -64,7 +65,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
> >
<CursorControl canvasInstance={canvasInstance} activeControl={activeControl} /> <CursorControl canvasInstance={canvasInstance} activeControl={activeControl} />
<MoveControl canvasInstance={canvasInstance} activeControl={activeControl} /> <MoveControl canvasInstance={canvasInstance} activeControl={activeControl} />
<RotateControl canvasInstance={canvasInstance} rotateAll={rotateAll} /> <RotateControl rotateFrame={rotateFrame} />
<hr /> <hr />

@ -16,18 +16,15 @@ import {
import { import {
Rotation, Rotation,
Canvas, } from 'reducers/interfaces';
} from 'cvat-canvas';
interface Props { interface Props {
canvasInstance: Canvas; rotateFrame(rotation: Rotation): void;
rotateAll: boolean;
} }
function RotateControl(props: Props): JSX.Element { function RotateControl(props: Props): JSX.Element {
const { const {
rotateAll, rotateFrame,
canvasInstance,
} = props; } = props;
return ( return (
@ -39,16 +36,14 @@ function RotateControl(props: Props): JSX.Element {
<Tooltip title='Rotate the image anticlockwise' placement='topRight'> <Tooltip title='Rotate the image anticlockwise' placement='topRight'>
<Icon <Icon
className='cvat-rotate-canvas-controls-left' className='cvat-rotate-canvas-controls-left'
onClick={(): void => canvasInstance onClick={(): void => rotateFrame(Rotation.ANTICLOCKWISE90)}
.rotate(Rotation.ANTICLOCKWISE90, rotateAll)}
component={RotateIcon} component={RotateIcon}
/> />
</Tooltip> </Tooltip>
<Tooltip title='Rotate the image clockwise' placement='topRight'> <Tooltip title='Rotate the image clockwise' placement='topRight'>
<Icon <Icon
className='cvat-rotate-canvas-controls-right' className='cvat-rotate-canvas-controls-right'
onClick={(): void => canvasInstance onClick={(): void => rotateFrame(Rotation.CLOCKWISE90)}
.rotate(Rotation.CLOCKWISE90, rotateAll)}
component={RotateIcon} component={RotateIcon}
/> />
</Tooltip> </Tooltip>

@ -45,6 +45,7 @@ interface StateToProps {
selectedStatesID: number[]; selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frameAngle: number;
frame: number; frame: number;
opacity: number; opacity: number;
colorBy: ColorBy; colorBy: ColorBy;
@ -105,6 +106,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
data: frameData, data: frameData,
number: frame, number: frame,
}, },
frameAngles,
}, },
annotations: { annotations: {
states: annotations, states: annotations,
@ -143,6 +145,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
canvasInstance, canvasInstance,
jobInstance, jobInstance,
frameData, frameData,
frameAngle: frameAngles[frame - jobInstance.startFrame],
frame, frame,
activatedStateID, activatedStateID,
selectedStatesID, selectedStatesID,

@ -11,11 +11,13 @@ import {
mergeObjects, mergeObjects,
groupObjects, groupObjects,
splitTrack, splitTrack,
rotateCurrentFrame,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import { import {
ActiveControl, ActiveControl,
CombinedState, CombinedState,
Rotation,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
@ -28,6 +30,7 @@ interface DispatchToProps {
mergeObjects(enabled: boolean): void; mergeObjects(enabled: boolean): void;
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void; splitTrack(enabled: boolean): void;
rotateFrame(angle: Rotation): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -63,6 +66,9 @@ function dispatchToProps(dispatch: any): DispatchToProps {
splitTrack(enabled: boolean): void { splitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled)); dispatch(splitTrack(enabled));
}, },
rotateFrame(rotation: Rotation): void {
dispatch(rotateCurrentFrame(rotation));
},
}; };
} }

@ -4,14 +4,12 @@
import { import {
Canvas, Canvas,
Rotation,
CanvasVersion, CanvasVersion,
RectDrawingMethod, RectDrawingMethod,
} from '../../cvat-canvas/src/typescript/canvas'; } from '../../cvat-canvas/src/typescript/canvas';
export { export {
Canvas, Canvas,
Rotation,
CanvasVersion, CanvasVersion,
RectDrawingMethod, RectDrawingMethod,
}; };

@ -44,6 +44,7 @@ const defaultState: AnnotationState = {
changeTime: null, changeTime: null,
}, },
playing: false, playing: false,
frameAngles: [],
}, },
drawing: { drawing: {
activeShapeType: ShapeType.RECTANGLE, activeShapeType: ShapeType.RECTANGLE,
@ -138,6 +139,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
number, number,
data, data,
}, },
frameAngles: Array(job.stopFrame - job.startFrame + 1).fill(0),
}, },
drawing: { drawing: {
...state.drawing, ...state.drawing,
@ -233,6 +235,17 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.ROTATE_FRAME: {
const { offset, angle, rotateAll } = action.payload;
return {
...state,
player: {
...state.player,
frameAngles: state.player.frameAngles.map((_angle: number, idx: number) => (
rotateAll || offset === idx ? angle : _angle)),
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS: { case AnnotationActionTypes.SAVE_ANNOTATIONS: {
return { return {
...state, ...state,

@ -275,6 +275,11 @@ export enum ContextMenuType {
CANVAS_SHAPE = 'canvas_shape', CANVAS_SHAPE = 'canvas_shape',
} }
export enum Rotation {
ANTICLOCKWISE90,
CLOCKWISE90,
}
export interface AnnotationState { export interface AnnotationState {
activities: { activities: {
loads: { loads: {
@ -308,6 +313,7 @@ export interface AnnotationState {
changeTime: number | null; changeTime: number | null;
}; };
playing: boolean; playing: boolean;
frameAngles: number[];
}; };
drawing: { drawing: {
activeShapeType: ShapeType; activeShapeType: ShapeType;

Loading…
Cancel
Save