From 5051249cc986c90b18580de9431a00c20e54827f Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Tue, 30 Mar 2021 07:59:42 +0300 Subject: [PATCH] Crop polygon properly (#3025) * crop polygon properly * updated license headers and cvat-canvas version * updated changelog * fixed eslint errors * fixed eslint issues Co-authored-by: Boris Sekachev --- CHANGELOG.md | 1 + cvat-canvas/package-lock.json | 2 +- cvat-canvas/package.json | 2 +- cvat-canvas/src/typescript/cuboid.ts | 21 ++-- cvat-canvas/src/typescript/drawHandler.ts | 147 +++++++++++++++++----- cvat-canvas/src/typescript/shared.ts | 6 +- 6 files changed, 131 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a2c1779462..97c021e04574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed image quality option for tasks created from images () - Incorrect text on the warning when specifying an incorrect link to the issue tracker () - Updating label attributes when label contains number attributes () +- Crop a polygon if its points are outside the bounds of the image () ### Security diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 3da4a7eb8b58..f7760b71c061 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index cfd5365b2d98..486a71a7af34 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.4.0", + "version": "2.4.1", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/cuboid.ts b/cvat-canvas/src/typescript/cuboid.ts index 920bc39ec8d7..ded917141e2c 100644 --- a/cvat-canvas/src/typescript/cuboid.ts +++ b/cvat-canvas/src/typescript/cuboid.ts @@ -1,9 +1,9 @@ -import consts from './consts'; +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT -export interface Point { - x: number; - y: number; -} +import consts from './consts'; +import { Point } from './shared'; export enum Orientation { LEFT = 'left', @@ -17,7 +17,7 @@ function line(p1: Point, p2: Point): number[] { return [a, b, c]; } -function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null { +export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null { const L1 = line(p1, p2); const L2 = line(p3, p4); @@ -27,7 +27,7 @@ function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null let x = null; let y = null; - if (D !== 0) { + if (Math.abs(D) > Number.EPSILON) { x = Dx / D; y = Dy / D; return { x, y }; @@ -348,10 +348,9 @@ function setupCuboidPoints(points: Point[]): any[] { let p3; let p4; - const height = - Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x) - ? Math.abs(points[1].y - points[0].y) - : Math.abs(points[1].y - points[2].y); + const height = Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x) + ? Math.abs(points[1].y - points[0].y) + : Math.abs(points[1].y - points[2].y); // seperate into left and right point // we pick the first and third point because we know assume they will be on diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index c0ff78cab626..d1dc1cc40b70 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2020 Intel Corporation +// Copyright (C) 2019-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -15,12 +15,15 @@ import { pointsToNumberArray, BBox, Box, + Point, } from './shared'; import Crosshair from './crosshair'; import consts from './consts'; -import { DrawData, Geometry, RectDrawingMethod, Configuration, CuboidDrawingMethod } from './canvasModel'; +import { + DrawData, Geometry, RectDrawingMethod, Configuration, CuboidDrawingMethod, +} from './canvasModel'; -import { cuboidFrom4Points } from './cuboid'; +import { cuboidFrom4Points, intersection } from './cuboid'; export interface DrawHandler { configurate(configuration: Configuration): void; @@ -73,11 +76,11 @@ export class DrawHandlerImpl implements DrawHandler { private getFinalPolyshapeCoordinates( targetPoints: number[], ): { - points: number[]; - box: Box; - } { + points: number[]; + box: Box; + } { const { offset } = this.geometry; - const points = targetPoints.map((coord: number): number => coord - offset); + let points = targetPoints.map((coord: number): number => coord - offset); const box = { xtl: Number.MAX_SAFE_INTEGER, ytl: Number.MAX_SAFE_INTEGER, @@ -87,10 +90,91 @@ export class DrawHandlerImpl implements DrawHandler { const frameWidth = this.geometry.image.width; const frameHeight = this.geometry.image.height; - for (let i = 0; i < points.length - 1; i += 2) { - points[i] = Math.min(Math.max(points[i], 0), frameWidth); - points[i + 1] = Math.min(Math.max(points[i + 1], 0), frameHeight); + enum Direction { + Horizontal, + Vertical, + } + + const isBetween = (x1: number, x2: number, c: number): boolean => ( + c >= Math.min(x1, x2) && c <= Math.max(x1, x2) + ); + + const isInsideFrame = (p: Point, direction: Direction): boolean => { + if (direction === Direction.Horizontal) { + return isBetween(0, frameWidth, p.x); + } + return isBetween(0, frameHeight, p.y); + }; + + const findInersection = (p1: Point, p2: Point, p3: Point, p4: Point): number[] => { + const intersectionPoint = intersection(p1, p2, p3, p4); + if ( + intersectionPoint + && isBetween(p1.x, p2.x, intersectionPoint.x) + && isBetween(p1.y, p2.y, intersectionPoint.y) + ) { + return [intersectionPoint.x, intersectionPoint.y]; + } + return []; + }; + + const findIntersectionsWithFrameBorders = (p1: Point, p2: Point, direction: Direction): number[] => { + const resultPoints = []; + if (direction === Direction.Horizontal) { + resultPoints.push(...findInersection(p1, p2, { x: 0, y: 0 }, { x: 0, y: frameHeight })); + resultPoints.push( + ...findInersection(p1, p2, { x: frameWidth, y: frameHeight }, { x: frameWidth, y: 0 }), + ); + } else { + resultPoints.push( + ...findInersection(p1, p2, { x: 0, y: frameHeight }, { x: frameWidth, y: frameHeight }), + ); + resultPoints.push(...findInersection(p1, p2, { x: frameWidth, y: 0 }, { x: 0, y: 0 })); + } + + if (resultPoints.length === 4) { + if ( + Math.sign(resultPoints[0] - resultPoints[2]) !== Math.sign(p1.x - p2.x) + && Math.sign(resultPoints[1] - resultPoints[3]) !== Math.sign(p1.y - p2.y) + ) { + [resultPoints[0], resultPoints[2]] = [resultPoints[2], resultPoints[0]]; + [resultPoints[1], resultPoints[3]] = [resultPoints[3], resultPoints[1]]; + } + } + return resultPoints; + }; + + const crop = (polygonPoints: number[], direction: Direction): number[] => { + const resultPoints = []; + for (let i = 0; i < polygonPoints.length - 1; i += 2) { + const curPoint = { x: polygonPoints[i], y: polygonPoints[i + 1] }; + if (isInsideFrame(curPoint, direction)) { + resultPoints.push(polygonPoints[i], polygonPoints[i + 1]); + } + const isLastPoint = i === polygonPoints.length - 2; + if ( + isLastPoint + && (this.drawData.shapeType === 'polyline' + || (this.drawData.shapeType === 'polygon' && polygonPoints.length === 4)) + ) { + break; + } + const nextPoint = isLastPoint + ? { x: polygonPoints[0], y: polygonPoints[1] } + : { x: polygonPoints[i + 2], y: polygonPoints[i + 3] }; + const intersectionPoints = findIntersectionsWithFrameBorders(curPoint, nextPoint, direction); + if (intersectionPoints.length !== 0) { + resultPoints.push(...intersectionPoints); + } + } + return resultPoints; + }; + + points = crop(points, Direction.Horizontal); + points = crop(points, Direction.Vertical); + + for (let i = 0; i < points.length - 1; i += 2) { box.xtl = Math.min(box.xtl, points[i]); box.ytl = Math.min(box.ytl, points[i + 1]); box.xbr = Math.max(box.xbr, points[i]); @@ -106,9 +190,9 @@ export class DrawHandlerImpl implements DrawHandler { private getFinalCuboidCoordinates( targetPoints: number[], ): { - points: number[]; - box: Box; - } { + points: number[]; + box: Box; + } { const { offset } = this.geometry; let points = targetPoints; @@ -154,8 +238,8 @@ export class DrawHandlerImpl implements DrawHandler { if (cuboidOffsets.length === points.length / 2) { cuboidOffsets.forEach((offsetCoords: number[]): void => { - if (Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2) < minCuboidOffset.d) { - minCuboidOffset.d = Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2); + if (Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2)) < minCuboidOffset.d) { + minCuboidOffset.d = Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2)); [minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords; } }); @@ -213,8 +297,8 @@ export class DrawHandlerImpl implements DrawHandler { // We check if it is activated with remember function if (this.drawInstance.remember('_paintHandler')) { if ( - this.drawData.shapeType !== 'rectangle' && - this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC + this.drawData.shapeType !== 'rectangle' + && this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC ) { // Check for unsaved drawn shapes this.drawInstance.draw('done'); @@ -365,7 +449,8 @@ export class DrawHandlerImpl implements DrawHandler { } else { this.drawInstance.draw('update', e); const deltaTreshold = 15; - const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2); + const delta = Math.sqrt(((e.clientX - lastDrawnPoint.x) ** 2) + + ((e.clientY - lastDrawnPoint.y) ** 2)); if (delta > deltaTreshold) { this.drawInstance.draw('point', e); } @@ -386,17 +471,16 @@ export class DrawHandlerImpl implements DrawHandler { this.drawInstance.on('drawdone', (e: CustomEvent): void => { const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points')); const { shapeType, redraw: clientID } = this.drawData; - const { points, box } = - shapeType === 'cuboid' - ? this.getFinalCuboidCoordinates(targetPoints) - : this.getFinalPolyshapeCoordinates(targetPoints); + const { points, box } = shapeType === 'cuboid' + ? this.getFinalCuboidCoordinates(targetPoints) + : this.getFinalPolyshapeCoordinates(targetPoints); this.release(); if (this.canceled) return; if ( - shapeType === 'polygon' && - (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD && - points.length >= 3 * 2 + shapeType === 'polygon' + && (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD + && points.length >= 3 * 2 ) { this.onDrawDone( { @@ -407,9 +491,9 @@ export class DrawHandlerImpl implements DrawHandler { Date.now() - this.startTimestamp, ); } else if ( - shapeType === 'polyline' && - (box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) && - points.length >= 2 * 2 + shapeType === 'polyline' + && (box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) + && points.length >= 2 * 2 ) { this.onDrawDone( { @@ -527,10 +611,9 @@ export class DrawHandlerImpl implements DrawHandler { .split(/[,\s]/g) .map((coord: string): number => +coord); - const { points } = - this.drawData.initialState.shapeType === 'cuboid' - ? this.getFinalCuboidCoordinates(targetPoints) - : this.getFinalPolyshapeCoordinates(targetPoints); + const { points } = this.drawData.initialState.shapeType === 'cuboid' + ? this.getFinalCuboidCoordinates(targetPoints) + : this.getFinalPolyshapeCoordinates(targetPoints); if (!e.detail.originalEvent.ctrlKey) { this.release(); diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 5eb09324ec4d..55790f05409f 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2020 Intel Corporation +// Copyright (C) 2019-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -25,7 +25,7 @@ export interface BBox { y: number; } -interface Point { +export interface Point { x: number; y: number; } @@ -176,5 +176,5 @@ export function scalarProduct(a: Vector2D, b: Vector2D): number { } export function vectorLength(vector: Vector2D): number { - return Math.sqrt(vector.i ** 2 + vector.j ** 2); + return Math.sqrt((vector.i ** 2) + (vector.j ** 2)); }