diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c93361d55975..3df2b6f89b7b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -42,7 +42,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18.x' + node-version: '22.x' - name: Install npm packages working-directory: ./site diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index b79ec61f4b63..8ba58cff406d 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '22.x' - name: Install dependencies run: | diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index 17bcaf758409..24402c5b7291 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -256,7 +256,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '22.x' - name: Download CVAT server image uses: actions/download-artifact@v4 diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml deleted file mode 100644 index 88865f278d47..000000000000 --- a/.github/workflows/helm.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Helm -on: - push: - branches: - - 'master' - - 'develop' - pull_request: - types: [ready_for_review, opened, synchronize, reopened] - paths-ignore: - - 'site/**' - - '**/*.md' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - testing: - if: | - github.event.pull_request.draft == false && - !startsWith(github.event.pull_request.title, '[WIP]') && - !startsWith(github.event.pull_request.title, '[Dependent]') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Start minikube - uses: medyagh/setup-minikube@latest - with: - cpus: max - memory: max - - - name: Try the cluster! - run: kubectl get pods -A - - - name: Build images - run: | - export SHELL=/bin/bash - eval $(minikube -p minikube docker-env) - docker compose -f docker-compose.yml -f docker-compose.dev.yml build - echo -n "verifying images:" - docker images - - - uses: azure/setup-helm@v4 - with: - version: 'v3.9.4' - - - name: Deploy to minikube - run: | - printf " service:\n externalIPs:\n - $(minikube ip)\n" >> helm-chart/test.values.yaml - cd helm-chart - helm dependency update - cd .. - helm upgrade -n default release-${{ github.run_id }}-${{ github.run_attempt }} -i --create-namespace helm-chart -f helm-chart/values.yaml -f helm-chart/cvat.values.yaml -f helm-chart/test.values.yaml - - - name: Update test config - run: | - sed -i -e 's$http://localhost:8080$http://cvat.local:80$g' tests/python/shared/utils/config.py - find tests/python/shared/assets/ -type f -name '*.json' | xargs sed -i -e 's$http://localhost:8080$http://cvat.local$g' - echo "$(minikube ip) cvat.local" | sudo tee -a /etc/hosts - - - name: Wait for CVAT to be ready - run: | - max_tries=60 - while [[ $(kubectl get pods -l component=server -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for CVAT pod" && (( max_tries-- )) && sleep 5; done - while [[ $(kubectl get pods -l app.kubernetes.io/name=postgresql -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for DB pod" && (( max_tries-- )) && sleep 5; done - while [[ $(curl -s -o /tmp/server_response -w "%{http_code}" cvat.local/api/server/about) != "200" && max_tries -gt 0 ]]; do echo "waiting for CVAT" && (( max_tries-- )) && sleep 5; done - kubectl get pods - kubectl logs $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}') - - - - name: Generate SDK - run: | - pip3 install --user -r cvat-sdk/gen/requirements.txt - ./cvat-sdk/gen/generate.sh - - - name: Install test requirements - run: | - pip3 install --user cvat-sdk/ - pip3 install --user cvat-cli/ - pip3 install --user -r tests/python/requirements.txt - - - name: REST API and SDK tests - # We don't have external services in Helm tests, so we ignore corresponding cases - # They are still tested without Helm - run: | - kubectl cp tests/mounted_file_share/images $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}'):/home/django/share - kubectl cp tests/mounted_file_share/videos $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}'):/home/django/share - pytest --timeout 30 --platform=kube -m "not with_external_services" tests/python --log-cli-level DEBUG - - - name: Creating a log file from "cvat" container logs - if: failure() - env: - LOGS_DIR: "${{ github.workspace }}/rest_api_testing" - run: | - mkdir ${LOGS_DIR} - kubectl logs $(kubectl get pods -l component=server -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_server.log - kubectl logs $(kubectl get pods -l component=worker-import -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_worker_import.log - kubectl logs $(kubectl get pods -l component=worker-export -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_worker_export.log - kubectl logs $(kubectl get pods -l component=worker-webhooks -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_worker_webhooks.log - kubectl logs $(kubectl get pods -l app.kubernetes.io/name=traefik -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/traefik.log - - - name: Uploading "cvat" container logs as an artifact - if: failure() - uses: actions/upload-artifact@v4 - with: - name: rest_api_container_logs - path: "${{ github.workspace }}/rest_api_testing" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4845ac6ea50..93a9ae35fc8c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -280,7 +280,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '18.x' + node-version: '22.x' - name: Download CVAT server image uses: actions/download-artifact@v4 @@ -383,6 +383,109 @@ jobs: name: cypress_videos_${{ matrix.specs }} path: ${{ github.workspace }}/tests/cypress/videos + helm_rest_api_testing: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Start minikube + uses: medyagh/setup-minikube@latest + with: + cpus: max + memory: max + + - name: Try the cluster + run: kubectl get pods -A + + - name: Download CVAT server image + uses: actions/download-artifact@v4 + with: + name: cvat_server + path: /tmp/cvat_server/ + + - name: Download CVAT UI images + uses: actions/download-artifact@v4 + with: + name: cvat_ui + path: /tmp/cvat_ui/ + + - name: Load images + run: | + eval $(minikube -p minikube docker-env) + docker load --input /tmp/cvat_server/image.tar + docker load --input /tmp/cvat_ui/image.tar + docker image ls -a + + - uses: azure/setup-helm@v4 + + - name: Update Helm chart dependencies + working-directory: helm-chart + run: | + helm dependency update + + - name: Deploy to minikube + run: | + printf " service:\n externalIPs:\n - $(minikube ip)\n" >> helm-chart/test.values.yaml + helm upgrade release-${{ github.run_id }}-${{ github.run_attempt }} --install helm-chart \ + -f helm-chart/cvat.values.yaml \ + -f helm-chart/test.values.yaml \ + --set cvat.backend.tag=${{ env.CVAT_VERSION }} \ + --set cvat.frontend.tag=${{ env.CVAT_VERSION }} + + - name: Update test config + run: | + sed -i -e 's$http://localhost:8080$http://cvat.local:80$g' tests/python/shared/utils/config.py + find tests/python/shared/assets/ -type f -name '*.json' | xargs sed -i -e 's$http://localhost:8080$http://cvat.local$g' + echo "$(minikube ip) cvat.local" | sudo tee -a /etc/hosts + + - name: Generate SDK + run: | + pip3 install --user -r cvat-sdk/gen/requirements.txt + ./cvat-sdk/gen/generate.sh + + - name: Install test requirements + run: | + pip3 install --user cvat-sdk/ + pip3 install --user cvat-cli/ + pip3 install --user -r tests/python/requirements.txt + + - name: Wait for CVAT to be ready + run: | + max_tries=60 + while [[ $(kubectl get pods -l component=server -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for CVAT pod" && (( max_tries-- )) && sleep 5; done + while [[ $(kubectl get pods -l app.kubernetes.io/name=postgresql -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for DB pod" && (( max_tries-- )) && sleep 5; done + while [[ $(curl -s -o /tmp/server_response -w "%{http_code}" cvat.local/api/server/about) != "200" && max_tries -gt 0 ]]; do echo "waiting for CVAT" && (( max_tries-- )) && sleep 5; done + kubectl get pods + kubectl logs $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}') + + - name: REST API and SDK tests + # We don't have external services in Helm tests, so we ignore corresponding cases + # They are still tested without Helm + run: | + kubectl cp tests/mounted_file_share/images $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}'):/home/django/share + kubectl cp tests/mounted_file_share/videos $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}'):/home/django/share + pytest --timeout 30 --platform=kube -m "not with_external_services" tests/python --log-cli-level DEBUG + + - name: Creating a log file from "cvat" container logs + if: failure() + env: + LOGS_DIR: "${{ github.workspace }}/rest_api_testing" + run: | + mkdir ${LOGS_DIR} + kubectl logs $(kubectl get pods -l component=server -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_server.log + kubectl logs $(kubectl get pods -l component=worker-import -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_worker_import.log + kubectl logs $(kubectl get pods -l component=worker-export -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_worker_export.log + kubectl logs $(kubectl get pods -l component=worker-webhooks -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/cvat_worker_webhooks.log + kubectl logs $(kubectl get pods -l app.kubernetes.io/name=traefik -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/traefik.log + + - name: Uploading "cvat" container logs as an artifact + if: failure() + uses: actions/upload-artifact@v4 + with: + name: helm_rest_api_container_logs + path: "${{ github.workspace }}/rest_api_testing" + publish_dev_images: if: github.ref == 'refs/heads/develop' needs: [rest_api_testing, unit_testing, e2e_testing] diff --git a/.github/workflows/remark.yml b/.github/workflows/remark.yml index cc4be9da3409..2a8cf6f42a18 100644 --- a/.github/workflows/remark.yml +++ b/.github/workflows/remark.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '22.x' - name: Run checks run: | diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index a2d680226aeb..794a2ebb8b27 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -180,7 +180,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '22.x' - name: Download CVAT server image uses: actions/download-artifact@v4 diff --git a/.github/workflows/stylelint.yml b/.github/workflows/stylelint.yml index 21eb9d5042dd..1acad4ca956b 100644 --- a/.github/workflows/stylelint.yml +++ b/.github/workflows/stylelint.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '22.x' - name: Install dependencies run: | diff --git a/.github/workflows/update-yarn-lock.yml b/.github/workflows/update-yarn-lock.yml index 6f8ccd05a315..577f7c2b71c9 100644 --- a/.github/workflows/update-yarn-lock.yml +++ b/.github/workflows/update-yarn-lock.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '22.x' - name: Update yarn.lock file run: yarn diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e0ead66a21..519a73e00fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## \[2.28.0\] - 2025-02-06 + +### Added + +- Support for managing Redis migrations + () + +### Changed + +- Updated limitation for minimal object size from 9px area to 1px in dimensions + () + +### Fixed + +- Invalid chunks and backups after honeypot updates in tasks with cloud storage data + () + +- In some cases effect of drag/resize may be reset implicitly for a user + () + ## \[2.27.0\] - 2025-02-04 diff --git a/changelog.d/20250117_174701_maria_redis_migrations.md b/changelog.d/20250117_174701_maria_redis_migrations.md deleted file mode 100644 index 73f923ee357c..000000000000 --- a/changelog.d/20250117_174701_maria_redis_migrations.md +++ /dev/null @@ -1,4 +0,0 @@ -### Added - -- Support for managing Redis migrations - () diff --git a/changelog.d/20250129_130201_mzhiltso_fix_honeypot_changes_in_cs_tasks.md b/changelog.d/20250129_130201_mzhiltso_fix_honeypot_changes_in_cs_tasks.md deleted file mode 100644 index 53184cc3211f..000000000000 --- a/changelog.d/20250129_130201_mzhiltso_fix_honeypot_changes_in_cs_tasks.md +++ /dev/null @@ -1,4 +0,0 @@ -### Fixed - -- Invalid chunks and backups after honeypot updates in tasks with cloud storage data - () diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 60d51c369aff..1bf30e34ad9b 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -404,6 +404,32 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.style.cursor = ''; this.mode = Mode.IDLE; if (state && points) { + // we need to store "updated" and set "points" to an empty array + // as this information is used to define "updated" objects in diff logic during canvas objects setup + // if because of any reason updating was actually rejected somewhere, we must reset view inside this logic + + // there is one more deeper issue: + // somewhere canvas updates drawn views and then sends request, + // updating internal CVAT state (e.g. drag, resize) + // somewhere, however, it just sends request to update internal CVAT state + // (e.g. remove point, edit polygon/polyline) + // if object view was not changed by canvas and points accepted as is without any changes + // the view will not be updated during objects setup if we just set points as is here + // that is why we need to set points to an empty array (something that can't normally come from CVAT) + // I do not think it can be easily fixed now, hovewer in the future we should refactor code + if (Number.isInteger(state.parentID)) { + const { elements } = this.drawnStates[state.parentID]; + const drawnElement = elements.find((el) => el.clientID === state.clientID); + drawnElement.updated = 0; + drawnElement.points = []; + + this.drawnStates[state.parentID].updated = 0; + this.drawnStates[state.parentID].points = []; + } else { + this.drawnStates[state.clientID].updated = 0; + this.drawnStates[state.clientID].points = []; + } + const event: CustomEvent = new CustomEvent('canvas.edited', { bubbles: false, cancelable: true, diff --git a/cvat-canvas/src/typescript/consts.ts b/cvat-canvas/src/typescript/consts.ts index 3ea75dbb557d..1e39c1316879 100644 --- a/cvat-canvas/src/typescript/consts.ts +++ b/cvat-canvas/src/typescript/consts.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,8 +7,7 @@ const BASE_STROKE_WIDTH = 1.25; const BASE_GRID_WIDTH = 2; const BASE_POINT_SIZE = 4; const TEXT_MARGIN = 10; -const AREA_THRESHOLD = 9; -const SIZE_THRESHOLD = 3; +const SIZE_THRESHOLD = 1; const POINTS_STROKE_WIDTH = 1; const POINTS_SELECTED_STROKE_WIDTH = 4; const MIN_EDGE_LENGTH = 3; @@ -36,7 +36,6 @@ export default { BASE_GRID_WIDTH, BASE_POINT_SIZE, TEXT_MARGIN, - AREA_THRESHOLD, SIZE_THRESHOLD, POINTS_STROKE_WIDTH, POINTS_SELECTED_STROKE_WIDTH, diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index d54117c72957..ebee54109a04 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -47,16 +47,18 @@ interface FinalCoordinates { function checkConstraint(shapeType: string, points: number[], box: Box | null = null): boolean { if (shapeType === 'rectangle') { const [xtl, ytl, xbr, ybr] = points; - return (xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD; + const [width, height] = [xbr - xtl, ybr - ytl]; + return width >= consts.SIZE_THRESHOLD && height >= consts.SIZE_THRESHOLD; } if (shapeType === 'polygon') { - return (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD && points.length >= 3 * 2; + const [width, height] = [box.xbr - box.xtl, box.ybr - box.ytl]; + return (width >= consts.SIZE_THRESHOLD || height > consts.SIZE_THRESHOLD) && points.length >= 3 * 2; } if (shapeType === 'polyline') { - return (box.xbr - box.xtl >= consts.SIZE_THRESHOLD || - box.ybr - box.ytl >= consts.SIZE_THRESHOLD) && points.length >= 2 * 2; + const [width, height] = [box.xbr - box.xtl, box.ybr - box.ytl]; + return (width >= consts.SIZE_THRESHOLD || height >= consts.SIZE_THRESHOLD) && points.length >= 2 * 2; } if (shapeType === 'points') { @@ -64,18 +66,22 @@ function checkConstraint(shapeType: string, points: number[], box: Box | null = } if (shapeType === 'ellipse') { - const [rx, ry] = [points[2] - points[0], points[1] - points[3]]; - return rx * ry * Math.PI >= consts.AREA_THRESHOLD; + const [width, height] = [(points[2] - points[0]) * 2, (points[1] - points[3]) * 2]; + return width >= consts.SIZE_THRESHOLD && height > consts.SIZE_THRESHOLD; } if (shapeType === 'cuboid') { return points.length === 4 * 2 || points.length === 8 * 2 || - (points.length === 2 * 2 && (points[2] - points[0]) * (points[3] - points[1]) >= consts.AREA_THRESHOLD); + (points.length === 2 * 2 && + (points[2] - points[0]) >= consts.SIZE_THRESHOLD && + (points[3] - points[1]) >= consts.SIZE_THRESHOLD + ); } if (shapeType === 'skeleton') { const [xtl, ytl, xbr, ybr] = points; - return (xbr - xtl >= 1 || ybr - ytl >= 1); + const [width, height] = [xbr - xtl, ybr - ytl]; + return width >= consts.SIZE_THRESHOLD || height >= consts.SIZE_THRESHOLD; } return false; diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 9e210067e7d7..bde8cdbb8671 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -100,7 +100,7 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer: .fill('white') .addClass('cvat_canvas_text'), update(shape: SVG.Shape): void { - let text = `${Math.round(shape.width())}x${Math.round(shape.height())}px`; + let text = `${Math.floor(shape.width())}x${Math.floor(shape.height())}px`; if (shape.type === 'rect' || shape.type === 'ellipse') { let rotation = shape.transform().rotation || 0; // be sure, that rotation in range [0; 360] diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index 2bd32ba664f5..7e75d4e88ec2 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,4 +1,4 @@ -cvat-sdk==2.27.1 +cvat-sdk==2.28.1 attrs>=24.2.0 Pillow>=10.3.0 diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py index 063100886b02..5515a1a9a1ae 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.27.1" +VERSION = "2.28.1" diff --git a/cvat-core/src/object-utils.ts b/cvat-core/src/object-utils.ts index 12712032dca5..06a90a958924 100644 --- a/cvat-core/src/object-utils.ts +++ b/cvat-core/src/object-utils.ts @@ -67,45 +67,48 @@ export function findAngleDiff(rightAngle: number, leftAngle: number): number { } export function checkShapeArea(shapeType: ShapeType, points: number[]): boolean { - const MIN_SHAPE_LENGTH = 3; - const MIN_SHAPE_AREA = 9; - const MIN_MASK_SHAPE_AREA = 1; + const MIN_SHAPE_SIZE = 1; if (shapeType === ShapeType.POINTS) { return true; } + let width = 0; + let height = 0; + if (shapeType === ShapeType.MASK) { const [left, top, right, bottom] = points.slice(-4); - const area = (right - left + 1) * (bottom - top + 1); - return area >= MIN_MASK_SHAPE_AREA; - } - - if (shapeType === ShapeType.ELLIPSE) { + [width, height] = [right - left + 1, bottom - top + 1]; + } else if (shapeType === ShapeType.RECTANGLE) { + const [xtl, ytl, xbr, ybr] = points; + [width, height] = [xbr - xtl, ybr - ytl]; + } else if (shapeType === ShapeType.ELLIPSE) { const [cx, cy, rightX, topY] = points; - const [rx, ry] = [rightX - cx, cy - topY]; - return rx * ry * Math.PI > MIN_SHAPE_AREA; - } - - let xmin = Number.MAX_SAFE_INTEGER; - let xmax = Number.MIN_SAFE_INTEGER; - let ymin = Number.MAX_SAFE_INTEGER; - let ymax = Number.MIN_SAFE_INTEGER; + [width, height] = [(rightX - cx) * 2, (cy - topY) * 2]; + } else { + // polygon, polyline, cuboid, skeleton + let xmin = Number.MAX_SAFE_INTEGER; + let xmax = Number.MIN_SAFE_INTEGER; + let ymin = Number.MAX_SAFE_INTEGER; + let ymax = Number.MIN_SAFE_INTEGER; + + for (let i = 0; i < points.length - 1; i += 2) { + xmin = Math.min(xmin, points[i]); + xmax = Math.max(xmax, points[i]); + ymin = Math.min(ymin, points[i + 1]); + ymax = Math.max(ymax, points[i + 1]); + } - for (let i = 0; i < points.length - 1; i += 2) { - xmin = Math.min(xmin, points[i]); - xmax = Math.max(xmax, points[i]); - ymin = Math.min(ymin, points[i + 1]); - ymax = Math.max(ymax, points[i + 1]); - } + if ([ShapeType.POLYLINE, ShapeType.SKELETON, ShapeType.POLYGON].includes(shapeType)) { + // for polyshapes consider at least one dimension + // skeleton in corner cases may be a regular polyshape + return Math.max(xmax - xmin, ymax - ymin) >= MIN_SHAPE_SIZE; + } - if (shapeType === ShapeType.POLYLINE) { - const length = Math.max(xmax - xmin, ymax - ymin); - return length >= MIN_SHAPE_LENGTH; + [width, height] = [xmax - xmin, ymax - ymin]; } - const area = (xmax - xmin) * (ymax - ymin); - return area >= MIN_SHAPE_AREA; + return width >= MIN_SHAPE_SIZE && height >= MIN_SHAPE_SIZE; } export function rotatePoint(x: number, y: number, angle: number, cx = 0, cy = 0): number[] { diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index 17106556b638..8e3ed8469198 100755 --- a/cvat-sdk/gen/generate.sh +++ b/cvat-sdk/gen/generate.sh @@ -8,7 +8,7 @@ set -e GENERATOR_VERSION="v6.0.1" -VERSION="2.27.1" +VERSION="2.28.1" LIB_NAME="cvat_sdk" LAYER1_LIB_NAME="${LIB_NAME}/api_client" DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)" diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 058442d25575..1daa2292e3e9 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "2.27.1", + "version": "2.28.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx index e624e7c4adf0..c659daa8ac0e 100644 --- a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx +++ b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx @@ -80,8 +80,8 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element { const maxAutoSaveInterval = 60; const minAAMMargin = 0; const maxAAMMargin = 1000; - const minControlPointsSize = 4; - const maxControlPointsSize = 8; + const minControlPointsSize = 2; + const maxControlPointsSize = 10; return (
diff --git a/cvat/__init__.py b/cvat/__init__.py index 0b18ef317830..ced839eace75 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (2, 27, 1, "alpha", 0) +VERSION = (2, 28, 1, "alpha", 0) __version__ = get_version(VERSION) diff --git a/cvat/schema.yml b/cvat/schema.yml index a6a33a62aa7a..67f1af715f92 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.27.1 + version: 2.28.1 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: diff --git a/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js b/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js new file mode 100644 index 000000000000..51a599672aa3 --- /dev/null +++ b/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js @@ -0,0 +1,170 @@ +// Copyright (C) CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +/// + +const taskName = '5frames'; +const labelName = 'label'; +const attrName = 'attr1'; +const textDefaultValue = 'Some text'; +const issueId = '8952'; +const imagesCount = 5; +const width = 400; +const height = 400; +const posX = 50; +const posY = 50; +const color = 'white'; +const imageFileName = `image_${issueId}`; +const archiveName = `${imageFileName}.zip`; +const archivePath = `cypress/fixtures/${archiveName}`; +const imagesFolder = `cypress/fixtures/${imageFileName}`; + +const rect = [ + 30, + 30, + 30 + 34, + 30 + 23, +]; + +function translatePoints(points, delta, axis) { + if (axis === 'x') { + return [ + points[0] + delta, + points[1], + points[2] + delta, + points[3], + ]; + } + if (axis === 'y') { + return [ + points[0], + points[1] + delta, + points[2], + points[3] + delta, + ]; + } + return points; +} + +context('Create any track, check if track works correctly after deleting some frames', () => { + function readShapeCoords() { + return cy.get('.cvat_canvas_shape').then(($shape) => ({ + x: +$shape.attr('x'), + y: +$shape.attr('y'), + })); + } + + function validateShapeCoords({ x, y }) { + const precision = 0.01; // db server precision is 2 digits + cy.get('.cvat_canvas_shape').then(($shape) => { + const [xVal, yVal] = [ + +$shape.attr('x'), + +$shape.attr('y'), + ]; + expect(xVal).to.be.closeTo(x, precision); + expect(yVal).to.be.closeTo(y, precision); + }); + } + + describe('Description: user error, Could not receive frame 43 No one left position or right position was found. Interpolation impossible', () => { + let jobID = null; + const delta = 300; + before(() => { + cy.visit('/auth/login'); + cy.login(); + + // Create assets for task using nodeJS + cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); + cy.createZipArchive(imagesFolder, archivePath); + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName); + + cy.goToTaskList(); + cy.openTaskJob(taskName); + cy.url().should('contain', 'jobs').then((url) => { + const last = url.lastIndexOf('/'); + jobID = parseInt(url.slice(last + 1), 10); + }).then(() => { + // Remove all annotations and draw a track rect + const points0 = rect; + const points1 = translatePoints(points0, delta, 'x'); + const points2 = translatePoints(points1, delta, 'y'); + const track = { + shapes: [ + { + frame: 0, + type: 'rectangle', + points: points0, + }, + { + frame: 2, + type: 'rectangle', + points: points1, + }, + { + frame: 4, + type: 'rectangle', + points: points2, + }, + ], + frame: 0, + labelName, + objectType: 'track', + }; + cy.headlessCreateObjects([track], jobID); + }); + }); + + beforeEach(() => { + cy.headlessRestoreAllFrames(jobID); + + // Get job meta updates from the server and reload page to bring changes to UI + cy.reload(); + + cy.saveJob(); + cy.get('.cvat-player-first-button').click(); + }); + + it('Delete interpolated frames 0, 2, 4. Error should not appear', () => { + // Delete frames 0, 2, 4. Watch out for errors + cy.get('.cvat-player-first-button').click(); + cy.checkFrameNum(0); + cy.clickDeleteFrameAnnotationView(); + cy.checkFrameNum(1); + cy.goToNextFrame(2); + cy.clickDeleteFrameAnnotationView(); + cy.checkFrameNum(3); + cy.goToNextFrame(4); + cy.clickDeleteFrameAnnotationView(); + + // There should be no objects on the deleted frame + cy.get('.cvat_canvas_shape').should('not.exist'); + cy.clickSaveAnnotationView(); + + // Reopening a task with bad metadata might throw an exception that we can catch + cy.goToTaskList(); + cy.openTaskJob(taskName); + }); + + it('Change track positions on frames 2 and 4. Delete frame. Confirm same shape positions', () => { + cy.goCheckFrameNumber(2); + cy.clickDeleteFrameAnnotationView(); + cy.checkFrameNum(3); + cy.clickSaveAnnotationView(); + + let pos3 = null; + readShapeCoords().then((posOnFrame3) => { + pos3 = posOnFrame3; + cy.goToPreviousFrame(1); + }); + let pos1 = null; + readShapeCoords().then((posOnFrame1) => { + pos1 = posOnFrame1; + }); + cy.reload().then(() => { + cy.goToNextFrame(1).then(() => validateShapeCoords(pos1)); + cy.goToNextFrame(3).then(() => validateShapeCoords(pos3)); + }); + }); + }); +}); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index ba0c826e4cd6..ee2f3a12b780 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -348,6 +348,17 @@ Cypress.Commands.add('headlessCreateObjects', (objects, jobID) => { }); }); +Cypress.Commands.add('headlessRestoreAllFrames', (jobID) => { + cy.intercept('PATCH', `/api/jobs/${jobID}/data/meta**`).as('patchMeta'); + cy.window().then(async ($win) => { + await $win.cvat.server.request(`/api/jobs/${jobID}/data/meta`, { + method: 'PATCH', + data: { deleted_frames: [] }, + }); + }); + cy.wait('@patchMeta'); +}); + Cypress.Commands.add('headlessCreateTask', (taskSpec, dataSpec, extras) => { cy.window().then(async ($win) => { const task = new $win.cvat.classes.Task({