diff --git a/CHANGELOG.md b/CHANGELOG.md index ebdfbc6392a..a31fe722f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Tracking of multiple objects (30 and more) with TransT tracker () - - The issue azure.core.exceptions.ResourceExistsError: The specified blob already exists () +- The issue azure.core.exceptions.ResourceExistsError: The specified blob already exists () +- Image scaling when moving between images with different resolution () ### Security - TDB diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 731e96760c0..6806fb23f2a 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.16.3", + "version": "2.16.4", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 5f271f88b4d..2d5b19387be 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -77,6 +77,7 @@ export interface Configuration { shapeOpacity?: number; controlPointsSize?: number; outlinedBorders?: string | false; + resetZoom?: boolean; } export interface BrushTool { @@ -160,6 +161,7 @@ export enum UpdateReasons { IMAGE_ZOOMED = 'image_zoomed', IMAGE_FITTED = 'image_fitted', IMAGE_MOVED = 'image_moved', + IMAGE_ROTATED = 'image_rotated', GRID_UPDATED = 'grid_updated', ISSUE_REGIONS_UPDATED = 'issue_regions_updated', @@ -312,11 +314,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { imageIsDeleted: boolean; focusData: FocusData; gridSize: Size; - left: number; objects: any[]; issueRegions: Record; scale: number; top: number; + left: number; + fittedScale: number; zLayer: number | null; drawData: DrawData; editData: MasksEditData; @@ -355,6 +358,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { selectedShapeOpacity: 0.5, shapeOpacity: 0.2, outlinedBorders: false, + resetZoom: true, textFontSize: consts.DEFAULT_SHAPE_TEXT_SIZE, controlPointsSize: consts.BASE_POINT_SIZE, textPosition: consts.DEFAULT_SHAPE_TEXT_POSITION, @@ -378,11 +382,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { height: 100, width: 100, }, - left: 0, objects: [], issueRegions: {}, scale: 1, top: 0, + left: 0, + fittedScale: 0, zLayer: null, selected: null, mode: Mode.IDLE, @@ -506,6 +511,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { return; } + const relativeScaling = this.data.scale / this.data.fittedScale; + const prevImageLeft = this.data.left; + const prevImageTop = this.data.top; + const prevImageWidth = this.data.imageSize.width; + const prevImageHeight = this.data.imageSize.height; + this.data.imageSize = { height: frameData.height as number, width: frameData.width as number, @@ -516,6 +527,20 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (this.data.imageIsDeleted) { this.data.angle = 0; } + + this.fit(); + + // restore correct image position after switching to a new frame + // if corresponding option is disabled + // prevImageHeight and prevImageWidth are initialized by 0 by default + if (prevImageHeight !== 0 && prevImageWidth !== 0 && !this.data.configuration.resetZoom) { + const leftOffset = Math.round((this.data.imageSize.width - prevImageWidth) / 2); + const topOffset = Math.round((this.data.imageSize.height - prevImageHeight) / 2); + this.data.left = prevImageLeft - leftOffset; + this.data.top = prevImageTop - topOffset; + this.data.scale *= relativeScaling; + } + this.notify(UpdateReasons.IMAGE_CHANGED); this.data.zLayer = zLayer; this.data.objects = objectStates; @@ -562,7 +587,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { public rotate(rotationAngle: number): void { if (this.data.angle !== rotationAngle && !this.data.imageIsDeleted) { this.data.angle = (360 + Math.floor(rotationAngle / 90) * 90) % 360; - this.fit(); + this.notify(UpdateReasons.IMAGE_ROTATED); } } @@ -592,10 +617,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } this.data.scale = Math.min(Math.max(this.data.scale, FrameZoom.MIN), FrameZoom.MAX); - this.data.top = this.data.canvasSize.height / 2 - this.data.imageSize.height / 2; this.data.left = this.data.canvasSize.width / 2 - this.data.imageSize.width / 2; + // scale is changed during zooming or translating + // so, remember fitted scale to compute fit-relative scaling + this.data.fittedScale = this.data.scale; + this.notify(UpdateReasons.IMAGE_FITTED); } @@ -813,6 +841,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (typeof configuration.forceFrameUpdate === 'boolean') { this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate; } + if (typeof configuration.resetZoom === 'boolean') { + this.data.configuration.resetZoom = configuration.resetZoom; + } if (typeof configuration.selectedShapeOpacity === 'number') { this.data.configuration.selectedShapeOpacity = configuration.selectedShapeOpacity; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 410c2794310..be39a4697a2 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1220,6 +1220,12 @@ export class CanvasViewImpl implements CanvasView, Listener { // Setup event handlers this.canvas.addEventListener('dblclick', (e: MouseEvent): void => { this.controller.fit(); + this.canvas.dispatchEvent( + new CustomEvent('canvas.fit', { + bubbles: false, + cancelable: true, + }), + ); e.preventDefault(); }); @@ -1460,14 +1466,8 @@ export class CanvasViewImpl implements CanvasView, Listener { } else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) { this.moveCanvas(); this.transformCanvas(); - if (reason === UpdateReasons.IMAGE_FITTED) { - this.canvas.dispatchEvent( - new CustomEvent('canvas.fit', { - bubbles: false, - cancelable: true, - }), - ); - } + } else if (reason === UpdateReasons.IMAGE_ROTATED) { + this.transformCanvas(); } else if (reason === UpdateReasons.IMAGE_MOVED) { this.moveCanvas(); } else if (reason === UpdateReasons.OBJECTS_UPDATED) { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index cd440e5bd91..b9f6ac5ea73 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.50.8", + "version": "1.50.9", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx index b1ca5a2b764..480a8ab626f 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx @@ -359,6 +359,7 @@ class CanvasWrapperComponent extends React.PureComponent { colorBy, outlined, outlineColor, + resetZoom, } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas }; @@ -383,6 +384,7 @@ class CanvasWrapperComponent extends React.PureComponent { textFontSize, textPosition, textContent, + resetZoom, }); this.initialSetup(); @@ -439,7 +441,8 @@ class CanvasWrapperComponent extends React.PureComponent { prevProps.textContent !== textContent || prevProps.colorBy !== colorBy || prevProps.outlineColor !== outlineColor || - prevProps.outlined !== outlined + prevProps.outlined !== outlined || + prevProps.resetZoom !== resetZoom ) { canvasInstance.configure({ undefinedAttrValue: config.UNDEFINED_ATTRIBUTE_VALUE, @@ -456,6 +459,7 @@ class CanvasWrapperComponent extends React.PureComponent { controlPointsSize, textPosition, textContent, + resetZoom, }); } @@ -502,22 +506,16 @@ class CanvasWrapperComponent extends React.PureComponent { this.updateCanvas(); } - if (prevProps.frame !== frameData.number && resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION) { - canvasInstance.html().addEventListener( - 'canvas.setup', - () => { - canvasInstance.fit(); - }, - { once: true }, - ); - } - if (prevProps.showBitmap !== showBitmap) { canvasInstance.bitmap(showBitmap); } if (prevProps.frameAngle !== frameAngle) { canvasInstance.rotate(frameAngle); + if (prevProps.frameData === frameData) { + // explicitly rotated, not a new frame + canvasInstance.fit(); + } } if (prevProps.workspace !== workspace) { @@ -859,6 +857,8 @@ class CanvasWrapperComponent extends React.PureComponent { if (activatedState && activatedState.objectType !== ObjectType.TAG) { canvasInstance.activate(activatedStateID, activatedAttributeID); } + } else if (workspace === Workspace.ATTRIBUTE_ANNOTATION) { + canvasInstance.fit(); } } @@ -905,13 +905,11 @@ class CanvasWrapperComponent extends React.PureComponent { `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`, }); - // Events + canvasInstance.fitCanvas(); canvasInstance.html().addEventListener( 'canvas.setup', () => { const { activatedStateID, activatedAttributeID } = this.props; - canvasInstance.fitCanvas(); - canvasInstance.fit(); canvasInstance.activate(activatedStateID, activatedAttributeID); }, { once: true },