diff --git a/.editorconfig b/.editorconfig index 4c754efc9790..783dea0321dd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + +[*.{yml,yaml}] + +indent_size = 2 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 000000000000..8e72c8ae1376 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,59 @@ +name: Prepare release +on: + workflow_dispatch: + inputs: + newVersion: + description: "Version number for the new release" + required: true + default: X.Y.Z +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Validate version number + env: + NEW_VERSION: "${{ inputs.newVersion }}" + run: | + if ! [[ "$NEW_VERSION" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "Invalid version number" + exit 1 + fi + + - uses: actions/checkout@v4 + + - name: Verify that the release is new + run: | + if git ls-remote --exit-code origin refs/tags/v${{ inputs.newVersion }} > /dev/null; then + echo "Release v${{ inputs.newVersion }} already exists" + exit 1 + fi + + - name: Create release branch + run: + git checkout -b "release-${{ inputs.newVersion }}" + + - name: Collect changelog + run: + pipx run scriv collect --version="${{ inputs.newVersion }}" + + - name: Set the new version + run: + ./dev/update_version.py --set="${{ inputs.newVersion }}" + + - name: Commit release preparation changes + run: | + git -c user.name='github-actions[bot]' -c user.email='github-actions[bot]@users.noreply.github.com' \ + commit -a -m "Prepare release v${{ inputs.newVersion }}" + + - name: Push release branch + run: + git push -u origin "release-${{ inputs.newVersion }}" + + - name: Create release pull request + env: + GH_TOKEN: "${{ github.token }}" + run: | + gh pr create \ + --base=master \ + --title="Release v${{ inputs.newVersion }}" \ + --body="$(awk '/^## / { hn += 1; next } hn == 1 && !/^ + +## \[2.7.6\] - 2023-10-13 + +### Changed + +- Enabled nginx proxy buffering + () + +- Helm: set memory request for keydb + () + +- Supervisord (): + - added `autorestart=true` option for all workers + - unified program names to use dashes as delimiter instead of mixed '_' and '-' + - minor improvements to supervisor configurations + +### Removed + +- Removed gitter link from about modal + () + +### Fixed + +- Persist image filters across jobs + () + +- Splitting skeleton tracks on jobs + () + +- Uploading skeleton tracks in COCO Keypoints format + () + +- Fixed Siammask tracker error on grayscale images + () + +- Fixed memory leak on client side when event listener was not removed together with its context + () + +- Fixed crash related to issue tries to mount to not existing parent + () + +- Added 'notranslate' markers to avoid issues caused by extension translators + () + +- Getting CS content when S3 bucket contains manually created directories + () + +- Optimized huge memory consumption when working with masks in the interface + () + +### Security + +- Security upgrade opencv-python-headless from 4.5.5.62 to 4.8.1.78 + () + +- Added X-Frame-Options: deny + () + ## \[2.7.5\] - 2023-10-09 diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index e2fac71846e0..fd3145aa57cf 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.17.6", + "version": "2.18.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index cfe1c9ce8aab..01dbbcbbf997 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1796,7 +1796,7 @@ export class CanvasViewImpl implements CanvasView, Listener { if (state.shapeType === 'mask') { const { points } = state; const [left, top, right, bottom] = points.slice(-4); - const imageBitmap = expandChannels(255, 255, 255, points, 4); + const imageBitmap = expandChannels(255, 255, 255, points); imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, (dataURL: string) => new Promise((resolve) => { const img = document.createElement('img'); @@ -2893,7 +2893,7 @@ export class CanvasViewImpl implements CanvasView, Listener { const colorization = this.getShapeColorization(state); const color = fabric.Color.fromHex(colorization.fill).getSource(); const [left, top, right, bottom] = points.slice(-4); - const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4); + const imageBitmap = expandChannels(color[0], color[1], color[2], points); const image = this.adoptedContent.image().attr({ clientID: state.clientID, diff --git a/cvat-canvas/src/typescript/groupHandler.ts b/cvat-canvas/src/typescript/groupHandler.ts index f97a54c48991..9a6fe466c3fa 100644 --- a/cvat-canvas/src/typescript/groupHandler.ts +++ b/cvat-canvas/src/typescript/groupHandler.ts @@ -192,7 +192,7 @@ export class GroupHandlerImpl implements GroupHandler { const { points } = objectState; const colorRGB = [139, 0, 139]; const [left, top, right, bottom] = points.slice(-4); - const imageBitmap = expandChannels(colorRGB[0], colorRGB[1], colorRGB[2], points, 4); + const imageBitmap = expandChannels(colorRGB[0], colorRGB[1], colorRGB[2], points); const bbox = shape.bbox(); const image = this.canvas.image().attr({ diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index 076cbecafaf9..411abb5dff88 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -307,7 +307,7 @@ export class InteractionHandlerImpl implements InteractionHandler { this.selectize(true, this.drawnIntermediateShape, erroredShape); } else if (shapeType === 'mask') { const [left, top, right, bottom] = points.slice(-4); - const imageBitmap = expandChannels(255, 255, 255, points, 4); + const imageBitmap = expandChannels(255, 255, 255, points); const image = this.canvas.image().attr({ 'color-rendering': 'optimizeQuality', diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts index dfb9d6b11291..9ab4b18b0dfd 100644 --- a/cvat-canvas/src/typescript/masksHandler.ts +++ b/cvat-canvas/src/typescript/masksHandler.ts @@ -10,7 +10,7 @@ import { import consts from './consts'; import { DrawHandler } from './drawHandler'; import { - PropType, computeWrappingBox, alphaChannelOnly, expandChannels, imageDataToDataURL, + PropType, computeWrappingBox, zipChannels, expandChannels, imageDataToDataURL, } from './shared'; interface WrappingBBox { @@ -348,12 +348,12 @@ export class MasksHandlerImpl implements MasksHandler { const continueInserting = options.e.ctrlKey; const wrappingBbox = this.getDrawnObjectsWrappingBox(); const imageData = this.imageDataFromCanvas(wrappingBbox); - const alpha = alphaChannelOnly(imageData); - alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); + const rle = zipChannels(imageData); + rle.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); this.onDrawDone({ shapeType: this.drawData.shapeType, - points: alpha, + points: rle, }, Date.now() - this.startTimestamp, continueInserting, this.drawData); if (!continueInserting) { @@ -528,7 +528,7 @@ export class MasksHandlerImpl implements MasksHandler { const { points } = drawData.initialState; const color = fabric.Color.fromHex(this.getStateColor(drawData.initialState)).getSource(); const [left, top, right, bottom] = points.slice(-4); - const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4); + const imageBitmap = expandChannels(color[0], color[1], color[2], points); imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, (dataURL: string) => new Promise((resolve) => { fabric.Image.fromURL(dataURL, (image: fabric.Image) => { @@ -547,28 +547,29 @@ export class MasksHandlerImpl implements MasksHandler { })); this.isInsertion = true; - } else if (!this.isDrawing) { - // initialize drawing pipeline if not started - this.isDrawing = true; - this.redraw = drawData.redraw || null; + } else { + this.updateBrushTools(drawData.brushTool); + if (!this.isDrawing) { + // initialize drawing pipeline if not started + this.isDrawing = true; + this.redraw = drawData.redraw || null; + } } this.canvas.getElement().parentElement.style.display = 'block'; this.startTimestamp = Date.now(); } - this.updateBrushTools(drawData.brushTool); - if (!drawData.enabled && this.isDrawing) { try { if (this.drawnObjects.length) { const wrappingBbox = this.getDrawnObjectsWrappingBox(); const imageData = this.imageDataFromCanvas(wrappingBbox); - const alpha = alphaChannelOnly(imageData); - alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); + const rle = zipChannels(imageData); + rle.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); this.onDrawDone({ shapeType: this.drawData.shapeType, - points: alpha, + points: rle, ...(Number.isInteger(this.redraw) ? { clientID: this.redraw } : {}), }, Date.now() - this.startTimestamp, drawData.continue, this.drawData); } @@ -600,7 +601,7 @@ export class MasksHandlerImpl implements MasksHandler { const { points } = editData.state; const color = fabric.Color.fromHex(this.getStateColor(editData.state)).getSource(); const [left, top, right, bottom] = points.slice(-4); - const imageBitmap = expandChannels(color[0], color[1], color[2], points, 4); + const imageBitmap = expandChannels(color[0], color[1], color[2], points); imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, (dataURL: string) => new Promise((resolve) => { fabric.Image.fromURL(dataURL, (image: fabric.Image) => { @@ -634,9 +635,9 @@ export class MasksHandlerImpl implements MasksHandler { if (this.drawnObjects.length) { const wrappingBbox = this.getDrawnObjectsWrappingBox(); const imageData = this.imageDataFromCanvas(wrappingBbox); - const alpha = alphaChannelOnly(imageData); - alpha.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); - this.onEditDone(this.editData.state, alpha); + const rle = zipChannels(imageData); + rle.push(wrappingBbox.left, wrappingBbox.top, wrappingBbox.right, wrappingBbox.bottom); + this.onEditDone(this.editData.state, rle); } } finally { this.releaseEdit(); diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 9e89616a86f5..93dbec32b301 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -383,24 +383,53 @@ export function imageDataToDataURL( }, 'image/png'); } -export function alphaChannelOnly(imageData: Uint8ClampedArray): number[] { - const alpha = new Array(imageData.length / 4); +export function zipChannels(imageData: Uint8ClampedArray): number[] { + const rle = []; + + let prev = 0; + let summ = 0; for (let i = 3; i < imageData.length; i += 4) { - alpha[Math.floor(i / 4)] = imageData[i] > 0 ? 1 : 0; + const alpha = imageData[i] > 0 ? 1 : 0; + if (prev !== alpha) { + rle.push(summ); + prev = alpha; + summ = 1; + } else { + summ++; + } } - return alpha; + + rle.push(summ); + return rle; } -export function expandChannels(r: number, g: number, b: number, alpha: number[], endOffset = 0): Uint8ClampedArray { - const imageBitmap = new Uint8ClampedArray((alpha.length - endOffset) * 4); - for (let i = 0; i < alpha.length - endOffset; i++) { - const val = alpha[i] ? 1 : 0; - imageBitmap[i * 4] = r; - imageBitmap[i * 4 + 1] = g; - imageBitmap[i * 4 + 2] = b; - imageBitmap[i * 4 + 3] = val * 255; +export function expandChannels(r: number, g: number, b: number, encoded: number[]): Uint8ClampedArray { + function rle2Mask(rle: number[], width: number, height: number): Uint8ClampedArray { + const decoded = new Uint8ClampedArray(width * height * 4).fill(0); + const { length } = rle; + let decodedIdx = 0; + let value = 0; + let i = 0; + + while (i < length - 4) { + let count = rle[i]; + while (count > 0) { + decoded[decodedIdx + 0] = r; + decoded[decodedIdx + 1] = g; + decoded[decodedIdx + 2] = b; + decoded[decodedIdx + 3] = value * 255; + decodedIdx += 4; + count--; + } + i++; + value = Math.abs(value - 1); + } + + return decoded; } - return imageBitmap; + + const [left, top, right, bottom] = encoded.slice(-4); + return rle2Mask(encoded, right - left + 1, bottom - top + 1); } export type PropType = T[Prop]; diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index 383315d20fcf..1b4894170014 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,3 +1,3 @@ -cvat-sdk~=2.7.5 +cvat-sdk~=2.7.6 Pillow>=10.0.1 setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py index 0fcd1c9993a7..d91f62f701a1 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.7.5" +VERSION = "2.7.6" diff --git a/cvat-core/package.json b/cvat-core/package.json index 7449b052113a..68d40617f7cf 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "11.1.0", + "version": "12.0.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index afb9f636aa9a..93b01cb969d5 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -13,7 +13,7 @@ import Statistics from './statistics'; import { Label } from './labels'; import { ArgumentError, ScriptingError } from './exceptions'; import ObjectState from './object-state'; -import { mask2Rle, truncateMask } from './object-utils'; +import { cropMask } from './object-utils'; import config from './config'; import { HistoryActions, ShapeType, ObjectType, colors, Source, @@ -844,11 +844,7 @@ export default class Collection { occluded: state.occluded || false, points: state.shapeType === 'mask' ? (() => { const { width, height } = this.frameMeta[state.frame]; - const points = truncateMask(state.points, 0, width, height); - const [left, top, right, bottom] = points.splice(-4); - const rlePoints = mask2Rle(points); - rlePoints.push(left, top, right, bottom); - return rlePoints; + return cropMask(state.points, width, height); })() : state.points, rotation: state.rotation || 0, type: state.shapeType, diff --git a/cvat-core/src/annotations-objects.ts b/cvat-core/src/annotations-objects.ts index 99212d38bc90..1ad31ca00793 100644 --- a/cvat-core/src/annotations-objects.ts +++ b/cvat-core/src/annotations-objects.ts @@ -14,7 +14,7 @@ import { import AnnotationHistory from './annotations-history'; import { checkNumberOfPoints, attrsAsAnObject, checkShapeArea, mask2Rle, rle2Mask, - computeWrappingBox, findAngleDiff, rotatePoint, validateAttributeValue, truncateMask, + computeWrappingBox, findAngleDiff, rotatePoint, validateAttributeValue, cropMask, } from './object-utils'; const defaultGroupColor = '#E0E0E0'; @@ -2201,8 +2201,7 @@ export class MaskShape extends Shape { Annotation.prototype.validateStateBeforeSave.call(this, data, updated); if (updated.points) { const { width, height } = this.frameMeta[frame]; - const fittedPoints = truncateMask(data.points, 0, width, height); - return fittedPoints; + return cropMask(data.points, width, height); } return []; @@ -2264,7 +2263,7 @@ export class MaskShape extends Shape { const undoSource = this.source; const [redoLeft, redoTop, redoRight, redoBottom] = maskPoints.splice(-4); - const points = mask2Rle(maskPoints); + const points = maskPoints; const redoPoints = points; const redoSource = computeNewSource(this.source); @@ -2301,16 +2300,26 @@ export class MaskShape extends Shape { } } - static distance(points: number[], x: number, y: number): null | number { - const [left, top, right, bottom] = points.slice(-4); + static distance(rle: number[], x: number, y: number): null | number { + const [left, top, right, bottom] = rle.slice(-4); const [width, height] = [right - left + 1, bottom - top + 1]; const [translatedX, translatedY] = [x - left, y - top]; if (translatedX < 0 || translatedX >= width || translatedY < 0 || translatedY >= height) { return null; } + const offset = Math.floor(translatedY) * width + Math.floor(translatedX); + let sum = 0; + let value = 0; + + for (const count of rle) { + sum += count; + if (sum > offset) { + return value || null; + } + value = Math.abs(value - 1); + } - if (points[offset]) return 1; return null; } } @@ -2324,7 +2333,7 @@ MaskShape.prototype.toJSON = function () { MaskShape.prototype.get = function (frame) { const result = Shape.prototype.get.call(this, frame); - result.points = rle2Mask(this.points, this.right - this.left + 1, this.bottom - this.top + 1); + result.points = this.points.slice(0); result.points.push(this.left, this.top, this.right, this.bottom); return result; }; diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index d53b0fc88841..1c1b1c6ab895 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -28,6 +28,7 @@ import { Exception, ArgumentError, DataError, ScriptingError, ServerError, } from './exceptions'; +import { mask2Rle, rle2Mask } from './object-utils'; import User from './user'; import pjson from '../package.json'; import config from './config'; @@ -320,6 +321,10 @@ function build() { Webhook, AnnotationGuide, }, + utils: { + mask2Rle, + rle2Mask, + }, }; cvat.server = Object.freeze(cvat.server); @@ -340,8 +345,8 @@ function build() { cvat.organizations = Object.freeze(cvat.organizations); cvat.webhooks = Object.freeze(cvat.webhooks); cvat.analytics = Object.freeze(cvat.analytics); - cvat.storage = Object.freeze(cvat.storage); cvat.classes = Object.freeze(cvat.classes); + cvat.utils = Object.freeze(cvat.utils); const implemented = Object.freeze(implementAPI(cvat)); return implemented; diff --git a/cvat-core/src/exceptions.ts b/cvat-core/src/exceptions.ts index f418aca2cc0a..84a7ab8acd53 100644 --- a/cvat-core/src/exceptions.ts +++ b/cvat-core/src/exceptions.ts @@ -25,11 +25,6 @@ export class Exception extends Error { const line = info.lineNumber; const column = info.columnNumber; - // TODO: NOT IMPLEMENTED? - // const { - // jobID, taskID, clientID, projID, - // } = config; - Object.defineProperties( this, Object.freeze({ @@ -63,18 +58,6 @@ export class Exception extends Error { */ get: () => time, }, - // jobID: { - // get: () => jobID, - // }, - // taskID: { - // get: () => taskID, - // }, - // projID: { - // get: () => projID, - // }, - // clientID: { - // get: () => clientID, - // }, filename: { /** * @name filename diff --git a/cvat-core/src/object-utils.ts b/cvat-core/src/object-utils.ts index 46ee961b4f69..30a039503b81 100644 --- a/cvat-core/src/object-utils.ts +++ b/cvat-core/src/object-utils.ts @@ -176,61 +176,136 @@ export function validateAttributeValue(value: string, attr: Attribute): boolean return values.includes(value); } -export function truncateMask(points: number[], _: number, width: number, height: number): number[] { - const [currentLeft, currentTop, currentRight, currentBottom] = points.slice(-4); +// Method computes correct mask wrapping bbox +// Taking into account image size and removing leading/terminating zeros, minimizing the mask size +function findMaskBorders(rle: number[], width: number, height: number): { + top: number, + left: number, + right: number, + bottom: number, +} { + const [currentLeft, currentTop, currentRight, currentBottom] = rle.slice(-4); const [currentWidth, currentHeight] = [currentRight - currentLeft + 1, currentBottom - currentTop + 1]; + const empty = { + top: 0, + left: 0, + right: 0, + bottom: 0, + }; + + if (currentWidth < 0 || currentHeight < 0) { + return empty; + } + let x = 0; // mask-relative + let y = 0; // mask-relative + let value = 0; + + // first let's find actual wrapping bounding box + // cutting leading/terminating zeros from the mask let left = width; let right = 0; let top = height; let bottom = 0; let atLeastOnePixel = false; - const truncatedPoints = []; - - for (let y = 0; y < currentHeight; y++) { - const absY = y + currentTop; - for (let x = 0; x < currentWidth; x++) { + for (let idx = 0; idx < rle.length - 4; idx++) { + let count = rle[idx]; + while (count) { + // get image-relative coordinates + const absY = y + currentTop; const absX = x + currentLeft; - const offset = y * currentWidth + x; - if (absX >= width || absY >= height || absX < 0 || absY < 0) { - points[offset] = 0; + if (!(absX >= width || absY >= height || absX < 0 || absY < 0) && value) { + if (value) { + // update coordinates to fit them around non-zero values + atLeastOnePixel = true; + left = Math.min(left, absX); + top = Math.min(top, absY); + right = Math.max(right, absX); + bottom = Math.max(bottom, absY); + } } - if (points[offset]) { - atLeastOnePixel = true; - left = Math.min(left, absX); - top = Math.min(top, absY); - right = Math.max(right, absX); - bottom = Math.max(bottom, absY); + // shift coordinates and count + x++; + if (x === currentWidth) { + y++; + x = 0; } + count--; } + + // shift current rle value + value = Math.abs(value - 1); } if (!atLeastOnePixel) { - // if mask is empty, set its size as 0 - left = 0; - top = 0; + return empty; + } + + return { + top, left, right, bottom, + }; +} + +// Method performs cropping of a mask in RLE format +// It cuts mask parts that are out of the image width/height +// Also it cuts leading/terminating zeros and minimizes mask wrapping bounding box +export function cropMask(rle: number[], width: number, height: number): number[] { + const [currentLeft, currentTop, currentRight] = rle.slice(-4, -1); + const { + top, left, right, bottom, + } = findMaskBorders(rle, width, height); + + if (top === bottom || left === right) { + return [0, 0, 0, 0]; } - // TODO: check corner case when right = left = 0 - const [newWidth, newHeight] = [right - left + 1, bottom - top + 1]; - for (let y = 0; y < newHeight; y++) { - for (let x = 0; x < newWidth; x++) { - const leftDiff = left - currentLeft; - const topDiff = top - currentTop; - const offset = (y + topDiff) * currentWidth + (x + leftDiff); - truncatedPoints.push(points[offset]); + const maskWidth = currentRight - currentLeft + 1; + const croppedRLE = []; + + let x = 0; // mask-relative + let y = 0; // mask-relative + let value = 0; + let croppedCount = 0; + for (let idx = 0; idx < rle.length - 4; idx++) { + let count = rle[idx]; + while (count) { + // get image-relative coordinates + const absY = y + currentTop; + const absX = x + currentLeft; + + if (!(absX > right || absY > bottom || absX < left || absY < top)) { + // absolute coordinates stay within the image + croppedCount++; + } + + // shift coordinates and count + x++; + if (x === maskWidth) { + y++; + x = 0; + } + count--; + } + + // switch current rle value + value = Math.abs(value - 1); + if (croppedCount === 0 && croppedRLE.length) { + croppedCount = croppedRLE.pop(); + } else { + croppedRLE.push(croppedCount); + croppedCount = 0; } } - truncatedPoints.push(left, top, right, bottom); - if (!checkShapeArea(ShapeType.MASK, truncatedPoints)) { - return []; + croppedRLE.push(left, top, right, bottom); + if (!checkShapeArea(ShapeType.MASK, croppedRLE)) { + return [0, 0, 0, 0]; } - return truncatedPoints; + return croppedRLE; } export function mask2Rle(mask: number[]): number[] { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 7296764e092a..3929ecc9f35a 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -35,6 +35,8 @@ type Params = { action?: string, }; +tus.defaultOptions.storeFingerprintForResuming = false; + function enableOrganization(): { org: string } { return { org: config.organization.organizationSlug || '' }; } diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index d83e3c6b43c7..e20666f30be6 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.7.5" +VERSION="2.7.6" 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 4fbfa49cd725..f8d60f75383b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.57.2", + "version": "1.58.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/react_nginx.conf b/cvat-ui/react_nginx.conf index 28d70b4f33cc..29ae133f3978 100644 --- a/cvat-ui/react_nginx.conf +++ b/cvat-ui/react_nginx.conf @@ -15,11 +15,12 @@ server { location / { # Any route that doesn't exist on the server (e.g. /devices) try_files $uri $uri/ /index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Cross-Origin-Opener-Policy "same-origin"; - add_header Cross-Origin-Embedder-Policy "credentialless"; - add_header Expires 0; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Cross-Origin-Opener-Policy "same-origin"; + add_header Cross-Origin-Embedder-Policy "credentialless"; + add_header Expires 0; + add_header X-Frame-Options "deny"; } location /assets { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 076a35f3f04f..6953ef5ff5fe 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -581,7 +581,7 @@ export function switchPlay(playing: boolean): AnyAction { }; } -export function confirmCanvasReady(ranges?: string): AnyAction { +function confirmCanvasReady(ranges?: string): AnyAction { return { type: AnnotationActionTypes.CONFIRM_CANVAS_READY, payload: { ranges }, @@ -644,47 +644,22 @@ export function changeFrameAsync( throw Error(`Required frame ${toFrame} is out of the current job`); } - const abortAction = (): void => { - const currentState = getState(); - dispatch({ - type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, - payload: { - number: currentState.annotation.player.frame.number, - data: currentState.annotation.player.frame.data, - filename: currentState.annotation.player.frame.filename, - relatedFiles: currentState.annotation.player.frame.relatedFiles, - delay: currentState.annotation.player.frame.delay, - changeTime: currentState.annotation.player.frame.changeTime, - states: currentState.annotation.annotations.states, - minZ: currentState.annotation.annotations.zLayer.min, - maxZ: currentState.annotation.annotations.zLayer.max, - curZ: currentState.annotation.annotations.zLayer.cur, - }, - }); - - dispatch(confirmCanvasReady()); - }; - - dispatch({ - type: AnnotationActionTypes.CHANGE_FRAME, - payload: {}, - }); - if (toFrame === frame && !forceUpdate) { - abortAction(); return; } - const data = await job.frames.get(toFrame, fillBuffer, frameStep); - const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters); - if (!isAbleToChangeFrame() || statisticsVisible || propagateVisible) { - // while doing async actions above, canvas can become used by a user in another way - // so, we need an additional check and if it is used, we do not update state - abortAction(); return; } + const data = await job.frames.get(toFrame, fillBuffer, frameStep); + const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters); + + dispatch({ + type: AnnotationActionTypes.CHANGE_FRAME, + payload: {}, + }); + // commit the latest job frame to local storage localStorage.setItem(`Job_${job.id}_frame`, `${toFrame}`); await job.logger.log(LogType.changeFrame, { diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/gamma-filter.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/gamma-filter.tsx index cdd2d887f2ac..29c5709363f8 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/gamma-filter.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/gamma-filter.tsx @@ -46,6 +46,8 @@ export default function GammaFilter(): JSX.Element { useEffect(() => { if (filters.length === 0) { setGamma(1); + } else if (gammaFilter) { + setGamma((gammaFilter.modifier as GammaCorrection).gamma); } }, [filters]); diff --git a/cvat-ui/src/components/annotation-page/review/issues-aggregator.tsx b/cvat-ui/src/components/annotation-page/review/issues-aggregator.tsx index 9e59d32c4382..a855ec303ae0 100644 --- a/cvat-ui/src/components/annotation-page/review/issues-aggregator.tsx +++ b/cvat-ui/src/components/annotation-page/review/issues-aggregator.tsx @@ -86,7 +86,7 @@ export default function IssueAggregatorComponent(): JSX.Element | null { return () => { canvasInstance.html().removeEventListener('canvas.zoom', geometryListener); canvasInstance.html().removeEventListener('canvas.fit', geometryListener); - canvasInstance.html().addEventListener('canvas.reshape', geometryListener); + canvasInstance.html().removeEventListener('canvas.reshape', geometryListener); }; } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 86b4d01ee083..dbc0f75347b4 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -210,7 +210,7 @@ export class ToolsControlComponent extends React.PureComponent { id: string | null; isAborted: boolean; latestResponse: { - mask: number[][], + rle: number[], points: number[][], bounds?: [number, number, number, number], }; @@ -245,7 +245,7 @@ export class ToolsControlComponent extends React.PureComponent { id: null, isAborted: false, latestResponse: { - mask: [], + rle: [], points: [], }, lastestApproximatedPoints: [], @@ -291,7 +291,7 @@ export class ToolsControlComponent extends React.PureComponent { this.interaction = { id: null, isAborted: false, - latestResponse: { mask: [], points: [] }, + latestResponse: { rle: [], points: [] }, lastestApproximatedPoints: [], latestRequest: null, hideMessage: null, @@ -386,13 +386,27 @@ export class ToolsControlComponent extends React.PureComponent { // approximation with cv.approxPolyDP const approximated = await this.approximateResponsePoints(response.points); + const rle = core.utils.mask2Rle(response.mask.flat()); + if (response.bounds) { + rle.push(...response.bounds); + } else { + const height = response.mask.length; + const width = response.mask[0].length; + rle.push(0, 0, width - 1, height - 1); + } + + response.mask = rle; if (this.interaction.id !== interactionId || this.interaction.isAborted) { // new interaction session or the session is aborted return; } - this.interaction.latestResponse = response; + this.interaction.latestResponse = { + bounds: response.bounds, + points: response.points, + rle, + }; this.interaction.lastestApproximatedPoints = approximated; this.setState({ pointsReceived: !!response.points.length }); @@ -406,20 +420,12 @@ export class ToolsControlComponent extends React.PureComponent { } if (this.interaction.lastestApproximatedPoints.length) { - const maskPoints = this.interaction.latestResponse.mask.flat(); - if (this.interaction.latestResponse.bounds) { - maskPoints.push(...this.interaction.latestResponse.bounds); - } else { - const height = this.interaction.latestResponse.mask.length; - const width = this.interaction.latestResponse.mask[0].length; - maskPoints.push(0, 0, width - 1, height - 1); - } canvasInstance.interact({ enabled: true, intermediateShape: { shapeType: convertMasksToPolygons ? ShapeType.POLYGON : ShapeType.MASK, points: convertMasksToPolygons ? this.interaction.lastestApproximatedPoints.flat() : - maskPoints, + this.interaction.latestResponse.rle, }, onChangeToolsBlockerState: this.onChangeToolsBlockerState, }); @@ -455,7 +461,7 @@ export class ToolsControlComponent extends React.PureComponent { this.interaction.isAborted = true; this.interaction.latestRequest = null; if (this.interaction.lastestApproximatedPoints.length) { - this.constructFromPoints(this.interaction.lastestApproximatedPoints); + this.constructFromPoints(); } } else if (shapesUpdated) { const interactor = activeInteractor as MLModel; @@ -845,7 +851,7 @@ export class ToolsControlComponent extends React.PureComponent { } } - private async constructFromPoints(points: number[][]): Promise { + private async constructFromPoints(): Promise { const { convertMasksToPolygons } = this.state; const { frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations, @@ -858,29 +864,20 @@ export class ToolsControlComponent extends React.PureComponent { source: core.enums.Source.SEMI_AUTO, label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, shapeType: ShapeType.POLYGON, - points: points.flat(), + points: this.interaction.lastestApproximatedPoints.flat(), occluded: false, zOrder: curZOrder, }); createAnnotations(jobInstance, frame, [object]); } else { - const maskPoints = this.interaction.latestResponse.mask.flat(); - if (this.interaction.latestResponse.bounds) { - maskPoints.push(...this.interaction.latestResponse.bounds); - } else { - const height = this.interaction.latestResponse.mask.length; - const width = this.interaction.latestResponse.mask[0].length; - maskPoints.push(0, 0, width - 1, height - 1); - } - const object = new core.classes.ObjectState({ frame, objectType: ObjectType.SHAPE, source: core.enums.Source.SEMI_AUTO, label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null, shapeType: ShapeType.MASK, - points: maskPoints, + points: this.interaction.latestResponse.rle, occluded: false, zOrder: curZOrder, }); @@ -1239,6 +1236,9 @@ export class ToolsControlComponent extends React.PureComponent { objectType: ObjectType.SHAPE, frame, occluded: false, + rotation: [ + ShapeType.RECTANGLE, ShapeType.ELLIPSE, + ].includes(data.type) ? (data.rotation || 0) : 0, source: core.enums.Source.AUTO, attributes: (data.attributes as { name: string, value: string }[]) .reduce((acc, attr) => { @@ -1271,10 +1271,13 @@ export class ToolsControlComponent extends React.PureComponent { } if (data.type === 'mask') { + const [left, top, right, bottom] = data.mask.splice(-4); + const rle = core.utils.mask2Rle(data.mask); + rle.push(left, top, right, bottom); return new core.classes.ObjectState({ ...objectData, shapeType: data.type, - points: data.mask, + points: rle, }); } diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 340f8058b2b0..790369c637d3 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -142,7 +142,7 @@ function HeaderComponent(props: Props): JSX.Element { } = props; const { - CHANGELOG_URL, LICENSE_URL, GITTER_URL, GITHUB_URL, GUIDE_URL, DISCORD_URL, + CHANGELOG_URL, LICENSE_URL, GITHUB_URL, GUIDE_URL, DISCORD_URL, } = config; const history = useHistory(); @@ -184,20 +184,13 @@ function HeaderComponent(props: Props): JSX.Element { ), 10]); - aboutLinks.push([( - - - Need help? - - - ), 20]); aboutLinks.push([( Find us on Discord - ), 30]); + ), 20]); aboutLinks.push(...aboutPlugins.map(({ component: Component, weight }, index: number) => ( [, weight] as [JSX.Element, number] ))); diff --git a/cvat-ui/src/config.tsx b/cvat-ui/src/config.tsx index 55822eb042c4..8a1001aff3a2 100644 --- a/cvat-ui/src/config.tsx +++ b/cvat-ui/src/config.tsx @@ -9,7 +9,6 @@ const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__'; const NO_BREAK_SPACE = '\u00a0'; const CHANGELOG_URL = 'https://github.com/opencv/cvat/blob/develop/CHANGELOG.md'; const LICENSE_URL = 'https://github.com/opencv/cvat/blob/develop/LICENSE'; -const GITTER_URL = 'https://gitter.im/opencv-cvat'; const DISCORD_URL = 'https://discord.gg/fNR3eXfk6C'; const GITHUB_URL = 'https://github.com/opencv/cvat'; const GITHUB_IMAGE_URL = 'https://github.com/opencv/cvat/raw/develop/site/content/en/images/cvat.jpg'; @@ -133,7 +132,6 @@ export default { NO_BREAK_SPACE, CHANGELOG_URL, LICENSE_URL, - GITTER_URL, DISCORD_URL, GITHUB_URL, GITHUB_IMAGE_URL, diff --git a/cvat-ui/src/index.html b/cvat-ui/src/index.html index 78d096243027..65e4812fe512 100644 --- a/cvat-ui/src/index.html +++ b/cvat-ui/src/index.html @@ -1,5 +1,6 @@ @@ -10,6 +11,7 @@ + Computer Vision Annotation Tool - +
diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index fdbf1baa62f7..a3917329fd3d 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -462,6 +462,10 @@ export default (state = defaultState, action: AnyAction): SettingsState => { case BoundariesActionTypes.RESET_AFTER_ERROR: case AnnotationActionTypes.GET_JOB_SUCCESS: { const { job } = action.payload; + const filters = [...state.imageFilters]; + filters.forEach((imageFilter) => { + imageFilter.modifier.currentProcessedImage = null; + }); return { ...state, @@ -477,7 +481,7 @@ export default (state = defaultState, action: AnyAction): SettingsState => { } : {}), }, - imageFilters: [], + imageFilters: filters, }; } case AnnotationActionTypes.INTERACT_WITH_CANVAS: { diff --git a/cvat-ui/src/utils/fabric-wrapper/gamma-correciton.ts b/cvat-ui/src/utils/fabric-wrapper/gamma-correciton.ts index 4e5bb657c9a4..04a76fd7a580 100644 --- a/cvat-ui/src/utils/fabric-wrapper/gamma-correciton.ts +++ b/cvat-ui/src/utils/fabric-wrapper/gamma-correciton.ts @@ -10,6 +10,8 @@ export interface GammaFilterOptions { } export default class GammaCorrection extends FabricFilter { + #gamma: number[]; + constructor(options: GammaFilterOptions) { super(); @@ -22,5 +24,17 @@ export default class GammaCorrection extends FabricFilter { this.filter = new fabric.Image.filters.Gamma({ gamma, }); + this.#gamma = gamma; + } + + public configure(options: object): void { + super.configure(options); + + const { gamma: newGamma } = options as GammaFilterOptions; + this.#gamma = newGamma; + } + + get gamma(): number { + return this.#gamma[0]; } } diff --git a/cvat/__init__.py b/cvat/__init__.py index 191ad96a2a21..7e61d20eae56 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (2, 7, 5, 'final', 0) +VERSION = (2, 7, 6, 'final', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/dataset_manager/annotation.py b/cvat/apps/dataset_manager/annotation.py index e13a4349e6d7..2cb8a5a2f8b1 100644 --- a/cvat/apps/dataset_manager/annotation.py +++ b/cvat/apps/dataset_manager/annotation.py @@ -100,9 +100,12 @@ def filter_track_shapes(shapes): track = deepcopy(track_) segment_shapes = filter_track_shapes(deepcopy(track['shapes'])) + track["elements"] = [ + cls._slice_track(element, start, stop, dimension) + for element in track.get('elements', []) + ] + if len(segment_shapes) < len(track['shapes']): - for element in track.get('elements', []): - element = cls._slice_track(element, start, stop, dimension) interpolated_shapes = TrackManager.get_interpolated_shapes( track, start, stop, dimension) scoped_shapes = filter_track_shapes(interpolated_shapes) @@ -909,7 +912,7 @@ def propagate(shape, end_frame, *, included_frames=None): break # The track finishes here if prev_shape: - assert curr_frame > prev_shape["frame"] # Catch invalid tracks + assert curr_frame > prev_shape["frame"], f"{curr_frame} > {prev_shape['frame']}. Track id: {track['id']}" # Catch invalid tracks # Propagate attributes for attr in prev_shape["attributes"]: diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 5d6a48b6f029..6a121bf796ef 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -1786,6 +1786,7 @@ def _convert_shape(self, label=self.map_label(element.label, shape.label), attributes=element_attr)) + dm_attr["keyframe"] = any([element.attributes.get("keyframe") for element in elements]) anno = dm.Skeleton(elements, label=dm_label, attributes=dm_attr, group=dm_group, z_order=shape.z_order) else: @@ -1899,6 +1900,15 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa dm.AnnotationType.mask: ShapeType.MASK } + track_formats = [ + 'cvat', + 'datumaro', + 'sly_pointcloud', + 'coco', + 'coco_instances', + 'coco_person_keypoints' + ] + label_cat = dm_dataset.categories()[dm.AnnotationType.label] root_hint = find_dataset_root(dm_dataset, instance_data) @@ -1983,7 +1993,7 @@ def reduce_fn(acc, v): if ann.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' shape_type = shapes[ann.type] - if track_id is None or 'keyframe' not in ann.attributes or dm_dataset.format not in ['cvat', 'datumaro', 'sly_pointcloud']: + if track_id is None or 'keyframe' not in ann.attributes or dm_dataset.format not in track_formats: elements = [] if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: @@ -2050,7 +2060,7 @@ def reduce_fn(acc, v): if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: - element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), bool) is True + element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), bool, True) element_occluded = element.visibility[0] == dm.Points.Visibility.hidden element_outside = element.visibility[0] == dm.Points.Visibility.absent if not element_keyframe and not element_outside: @@ -2069,6 +2079,7 @@ def reduce_fn(acc, v): ] element_source = element.attributes.pop('source').lower() \ if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' + tracks[track_id]['elements'][element.label].shapes.append(instance_data.TrackedShape( type=shapes[element.type], frame=frame_number, diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index 81a49d6a26ff..01f4214e897a 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -161,7 +161,7 @@ def _add_missing_shape(self, track, first_shape): missing_shape = deepcopy(first_shape) missing_shape["frame"] = track["frame"] missing_shape["outside"] = True - missing_shape.pop("id") + missing_shape.pop("id", None) track["shapes"].append(missing_shape) def _correct_frame_of_tracked_shapes(self, track): diff --git a/cvat/apps/engine/cloud_provider.py b/cvat/apps/engine/cloud_provider.py index 4083ff71394e..ab9474de17f9 100644 --- a/cvat/apps/engine/cloud_provider.py +++ b/cvat/apps/engine/cloud_provider.py @@ -467,7 +467,7 @@ def _list_raw_content_on_one_page( **({'Prefix': prefix} if prefix else {}), **({'ContinuationToken': next_token} if next_token else {}), ) - files = [f['Key'] for f in response.get('Contents', [])] + files = [f['Key'] for f in response.get('Contents', []) if not f['Key'].endswith('/')] directories = [p['Prefix'] for p in response.get('CommonPrefixes', [])] return { diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 8b8e4fbe2c92..1445a1acdd3c 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -621,6 +621,9 @@ def reset(self): "source": "auto" } + if shape["type"] in ("rectangle", "ellipse"): + shape["rotation"] = anno.get("rotation", 0) + if anno["type"] == "mask" and "points" in anno and conv_mask_to_poly: shape["type"] = "polygon" shape["points"] = anno["points"] diff --git a/cvat/nginx.conf b/cvat/nginx.conf index c3a4f486f5f2..a0ea97a07d00 100644 --- a/cvat/nginx.conf +++ b/cvat/nginx.conf @@ -46,6 +46,8 @@ http { # previously used value client_max_body_size 1G; + add_header X-Frame-Options deny; + server_name _; location /static/ { @@ -73,7 +75,9 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_redirect off; - proxy_buffering off; + proxy_buffering on; + proxy_buffers 16 8k; + proxy_temp_path /tmp/nginx 1 2; proxy_pass http://uvicorn; } } diff --git a/cvat/requirements/base.in b/cvat/requirements/base.in index 810cfe5fd640..2e476238ee2d 100644 --- a/cvat/requirements/base.in +++ b/cvat/requirements/base.in @@ -32,7 +32,7 @@ furl==2.1.0 google-cloud-storage==1.42.0 natsort==8.0.0 numpy~=1.22.2 -opencv-python-headless==4.5.5.62 +opencv-python-headless~=4.8 # The package is used by pyunpack as a command line tool to support multiple # archives. Don't use as a python module because it has GPL license. diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 1eb978809170..d2b7fb84e5b0 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:efa1135fd6eb8ff1c784d632ee43797415192c71 +# SHA1:21132d38b706741ba8b04ac1eb24b9136208dd3d # # This file is autogenerated by pip-compile-multi # To update, run: diff --git a/cvat/schema.yml b/cvat/schema.yml index 89947c524412..7666ed09ecf6 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.7.5 + version: 2.7.6 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: diff --git a/dev/update_version.py b/dev/update_version.py index 28f364097176..6cdaf313f968 100755 --- a/dev/update_version.py +++ b/dev/update_version.py @@ -70,6 +70,11 @@ def increment_major(self) -> None: self.major += 1 self._set_default_minor() + def set(self, v: str) -> None: + self.major, self.minor, self.patch = map(int, v.split('.')) + self.prerelease = 'final' + self.prerelease_number = 0 + def _set_default_prerelease_number(self) -> None: self.prerelease_number = 0 @@ -177,6 +182,9 @@ def main() -> None: action_group.add_argument('--verify-current', action='store_true', help='Check that all version numbers are consistent') + action_group.add_argument('--set', metavar='X.Y.Z', + help='Set the version to the specified version') + args = parser.parse_args() version = get_current_version() @@ -207,6 +215,9 @@ def main() -> None: elif args.major: version.increment_major() + elif args.set is not None: + version.set(args.set) + else: assert False, "Unreachable code" diff --git a/docker-compose.yml b/docker-compose.yml index b857c95ddf2a..8d3edd914ebe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: cvat_server: container_name: cvat_server - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -73,7 +73,7 @@ services: cvat_utils: container_name: cvat_utils - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -98,7 +98,7 @@ services: cvat_worker_import: container_name: cvat_worker_import - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -121,7 +121,7 @@ services: cvat_worker_export: container_name: cvat_worker_export - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -144,7 +144,7 @@ services: cvat_worker_annotation: container_name: cvat_worker_annotation - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -167,7 +167,7 @@ services: cvat_worker_webhooks: container_name: cvat_worker_webhooks - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -191,7 +191,7 @@ services: cvat_worker_quality_reports: container_name: cvat_worker_quality_reports - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -213,7 +213,7 @@ services: cvat_worker_analytics_reports: container_name: cvat_worker_analytics_reports - image: cvat/server:${CVAT_VERSION:-v2.7.5} + image: cvat/server:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_redis @@ -236,7 +236,7 @@ services: cvat_ui: container_name: cvat_ui - image: cvat/ui:${CVAT_VERSION:-v2.7.5} + image: cvat/ui:${CVAT_VERSION:-v2.7.6} restart: always depends_on: - cvat_server diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index d5439dee7f71..87a13c98e995 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.10.0 +version: 0.10.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm-chart/test.values.yaml b/helm-chart/test.values.yaml index 8e85e3944166..5a5fa8fe6bab 100644 --- a/helm-chart/test.values.yaml +++ b/helm-chart/test.values.yaml @@ -27,9 +27,14 @@ cvat: frontend: imagePullPolicy: Never +keydb: + resources: + requests: + traefik: logs: general: level: DEBUG access: enabled: true + diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index c3b38efc7120..ba6b8479ee3f 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -104,7 +104,7 @@ cvat: additionalVolumeMounts: [] replicas: 1 image: cvat/server - tag: v2.7.5 + tag: v2.7.6 imagePullPolicy: Always permissionFix: enabled: true @@ -128,7 +128,7 @@ cvat: frontend: replicas: 1 image: cvat/ui - tag: v2.7.5 + tag: v2.7.6 imagePullPolicy: Always labels: {} # test: test @@ -252,6 +252,9 @@ keydb: - storage-provider: ["flash", "/data/flash"] - maxmemory: "5G" - maxmemory-policy: "allkeys-lfu" + resources: + requests: + memory: "7G" nuclio: enabled: false diff --git a/serverless/pytorch/foolwood/siammask/nuclio/main.py b/serverless/pytorch/foolwood/siammask/nuclio/main.py index 1376fc2b7761..1b701fc2d07f 100644 --- a/serverless/pytorch/foolwood/siammask/nuclio/main.py +++ b/serverless/pytorch/foolwood/siammask/nuclio/main.py @@ -20,6 +20,7 @@ def handler(context, event): shapes = data.get("shapes") states = data.get("states") image = Image.open(buf) + image = image.convert("RGB") # to make sure image comes in RGB results = { 'shapes': [], diff --git a/site/content/en/docs/enterprise/subscription-managment.md b/site/content/en/docs/enterprise/subscription-managment.md index 4d14acfd6acb..ad52e084a8ed 100644 --- a/site/content/en/docs/enterprise/subscription-managment.md +++ b/site/content/en/docs/enterprise/subscription-managment.md @@ -15,23 +15,23 @@ you'll learn how to take control of your subscriptions and manage them. See: - [Billing](#billing) - - [Pro plan](#pro-plan) + - [Solo plan](#solo-plan) - [Team plan](#team-plan) - [Payment methods](#payment-methods) - [Paying with bank transfer](#paying-with-bank-transfer) - - [Change payment method on Pro plan](#change-payment-method-on-pro-plan) + - [Change payment method on Solo plan](#change-payment-method-on-solo-plan) - [Change payment method on Team plan](#change-payment-method-on-team-plan) - [Adding and removing team members](#adding-and-removing-team-members) - - [Pro plan](#pro-plan-1) + - [Solo plan](#solo-plan-1) - [Team plan](#team-plan-1) - [Change plan](#change-plan) - [Can I subscribe to several plans?](#can-i-subscribe-to-several-plans) - [Cancel plan](#cancel-plan) - [What will happen to my data?](#what-will-happen-to-my-data) - - [Pro plan](#pro-plan-2) + - [Solo plan](#solo-plan-2) - [Team plan](#team-plan-2) - [Plan renewal](#plan-renewal) - - [Pro plan](#pro-plan-3) + - [Solo plan](#solo-plan-3) - [Team plan](#team-plan-3) ## Billing @@ -42,9 +42,9 @@ description of limitations for each plan. For more information, see: [Pricing Plans](https://www.cvat.ai/post/new-pricing-plans) -### Pro plan +### Solo plan -**Account/Month**: The **Pro** plan has a fixed price and is +**Account/Month**: The **Solo** plan has a fixed price and is designed **for personal use only**. It doesn't allow collaboration with team members, but removes all the other limits of the **Free** plan. @@ -74,7 +74,7 @@ This section describes how to change or add payment methods. To pay with bank transfer: -1. Go to the **Upgrade to Pro**/**Team plan**> **Get started**. +1. Go to the **Upgrade to Solo**/**Team plan**> **Get started**. 2. Click **US Bank Transfer**. 3. Upon successful completion of the payment, the you will receive a receipt via email. @@ -82,11 +82,11 @@ To pay with bank transfer: ![Bank Transfer Payment](/images/bank_transfer_payment.jpg) -### Change payment method on Pro plan +### Change payment method on Solo plan -Access Manage **Pro Plan** > **Manage** and click **+Add Payment Method** +Access Manage **Solo plan** > **Manage** and click **+Add Payment Method** -![Payment pro](/images/update_payment_pro.png) +![Payment pro](/images/update_payment_solo.png) ### Change payment method on Team plan @@ -99,7 +99,7 @@ Access **Manage Team Plan** > **Manage** and click **+Add Payment Method**. This section describes how to add team members to collaborate within one team. -### Pro plan +### Solo plan Not available. @@ -120,7 +120,7 @@ but next month you will pay less by the amount of unused funds. ## Change plan -The procedure is the same for both **Pro** and **Team** plans. +The procedure is the same for both **Solo** and **Team** plans. If for some reason you want to change your plan, you need to: @@ -132,7 +132,7 @@ If for some reason you want to change your plan, you need to: Paid plans are not mutually exclusive. You can have several active subscriptions, -for example, the **Pro** plan and several **Team** +for example, the **Solo** plan and several **Team** plans for different organizations. ## Cancel plan @@ -155,13 +155,13 @@ Following the one month, you will receive a notification requesting you to either remove the excess data or it will be deleted automatically. -### Pro plan +### Solo plan -Access **Manage Pro Plan** > **Manage** > **Cancel plan** +Access **Manage Solo plan** > **Manage** > **Cancel plan** Please, fill out the feedback form, to help us improve our platform. -![Cancel pro](/images/cancel_pro.gif) +![Cancel pro](/images/cancel_solo.gif) ### Team plan @@ -176,9 +176,9 @@ Please, fill out the feedback form, to help us improve our platform. This section describes how to renew your CVAT subscription -### Pro plan +### Solo plan -Access **Manage Pro Plan** > **Manage** > **Renew plan** +Access **Manage Solo plan** > **Manage** > **Renew plan** ### Team plan diff --git a/site/content/en/images/bank_transfer_payment.jpg b/site/content/en/images/bank_transfer_payment.jpg index cb4b730d53dc..059bc9dd5268 100644 Binary files a/site/content/en/images/bank_transfer_payment.jpg and b/site/content/en/images/bank_transfer_payment.jpg differ diff --git a/site/content/en/images/cancel_pro.gif b/site/content/en/images/cancel_pro.gif deleted file mode 100644 index 48a6b948dbda..000000000000 Binary files a/site/content/en/images/cancel_pro.gif and /dev/null differ diff --git a/site/content/en/images/cancel_solo.gif b/site/content/en/images/cancel_solo.gif new file mode 100644 index 000000000000..036a914142c1 Binary files /dev/null and b/site/content/en/images/cancel_solo.gif differ diff --git a/site/content/en/images/update_payment_pro.png b/site/content/en/images/update_payment_pro.png deleted file mode 100644 index 65e3bbf5bb58..000000000000 Binary files a/site/content/en/images/update_payment_pro.png and /dev/null differ diff --git a/site/content/en/images/update_payment_solo.png b/site/content/en/images/update_payment_solo.png new file mode 100644 index 000000000000..62f592940f7f Binary files /dev/null and b/site/content/en/images/update_payment_solo.png differ diff --git a/supervisord/server.conf b/supervisord/server.conf index 424525f1da6b..65243fb68ba5 100644 --- a/supervisord/server.conf +++ b/supervisord/server.conf @@ -28,7 +28,7 @@ autostart=true autorestart=true startretries=5 numprocs=1 -process_name=%(program_name)s-%(process_num)s +process_name=%(program_name)s-%(process_num)d [fcgi-program:uvicorn] socket=unix:///tmp/uvicorn.sock @@ -36,7 +36,7 @@ command=python3 -m uvicorn --fd 0 --forwarded-allow-ips='*' cvat.asgi:applicatio autorestart=true environment=CVAT_EVENTS_LOCAL_DB_FILENAME="events_%(process_num)03d.db" numprocs=%(ENV_NUMPROCS)s -process_name=%(program_name)s-%(process_num)s +process_name=%(program_name)s-%(process_num)d [program:smokescreen] command=smokescreen --listen-ip=127.0.0.1 %(ENV_SMOKESCREEN_OPTS)s diff --git a/supervisord/utils.conf b/supervisord/utils.conf index 329f7a7cb577..3c0509205960 100644 --- a/supervisord/utils.conf +++ b/supervisord/utils.conf @@ -18,24 +18,25 @@ pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live [program:rqscheduler] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic \ +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c \ "python3 ~/rqscheduler.py --host %(ENV_CVAT_REDIS_HOST)s --password '%(ENV_CVAT_REDIS_PASSWORD)s' -i 30 --path '%(ENV_HOME)s'" environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=1 [program:rqworker-notifications] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 notifications \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=1 -[program:rqworker_cleaning] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +[program:rqworker-cleaning] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 cleaning \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s -process_name=rqworker_cleaning_%(process_num)s \ No newline at end of file +process_name=%(program_name)s-%(process_num)d +autorestart=true diff --git a/supervisord/worker.analytics_reports.conf b/supervisord/worker.analytics_reports.conf index 7718a9202db5..3d29ee46ec6f 100644 --- a/supervisord/worker.analytics_reports.conf +++ b/supervisord/worker.analytics_reports.conf @@ -17,11 +17,12 @@ loglevel=debug ; info, debug, warn, trace pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live -[program:rqworker_analytics_reports] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +[program:rqworker-analytics-reports] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 analytics_reports \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s -process_name=%(program_name)s-%(process_num)s +process_name=%(program_name)s-%(process_num)d +autorestart=true diff --git a/supervisord/worker.annotation.conf b/supervisord/worker.annotation.conf index a9390f37d93d..32adcbb757b6 100644 --- a/supervisord/worker.annotation.conf +++ b/supervisord/worker.annotation.conf @@ -18,9 +18,11 @@ pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live [program:rqworker-annotation] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 annotation \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s +process_name=%(program_name)s-%(process_num)d +autorestart=true diff --git a/supervisord/worker.export.conf b/supervisord/worker.export.conf index 057ee8949121..1e8dd3e8b14d 100644 --- a/supervisord/worker.export.conf +++ b/supervisord/worker.export.conf @@ -18,10 +18,11 @@ pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live [program:rqworker-export] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 export \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s -process_name=%(program_name)s-%(process_num)s +process_name=%(program_name)s-%(process_num)d +autorestart=true diff --git a/supervisord/worker.import.conf b/supervisord/worker.import.conf index 5f87896af553..22dca6772c77 100644 --- a/supervisord/worker.import.conf +++ b/supervisord/worker.import.conf @@ -18,13 +18,14 @@ pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live [program:rqworker-import] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 import \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s -process_name=%(program_name)s-%(process_num)s +process_name=%(program_name)s-%(process_num)d +autorestart=true [program:clamav-update] diff --git a/supervisord/worker.quality_reports.conf b/supervisord/worker.quality_reports.conf index da1202660d7d..155afd4461c5 100644 --- a/supervisord/worker.quality_reports.conf +++ b/supervisord/worker.quality_reports.conf @@ -17,11 +17,12 @@ loglevel=debug ; info, debug, warn, trace pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live -[program:rqworker_quality_reports] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ +[program:rqworker-quality-reports] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c " \ exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 quality_reports \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s -process_name=rqworker_quality_reports_%(process_num)s +process_name=%(program_name)s-%(process_num)d +autorestart=true diff --git a/supervisord/worker.webhooks.conf b/supervisord/worker.webhooks.conf index bed1523d1e1b..b818f05082a0 100644 --- a/supervisord/worker.webhooks.conf +++ b/supervisord/worker.webhooks.conf @@ -17,13 +17,14 @@ loglevel=debug ; info, debug, warn, trace pidfile=/tmp/supervisord/supervisord.pid ; pidfile location childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live -[program:rqworker_webhooks] -command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic \ +[program:rqworker-webhooks] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -c \ "exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 webhooks \ --worker-class cvat.rqworker.DefaultWorker \ " environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" numprocs=%(ENV_NUMPROCS)s +process_name=%(program_name)s-%(process_num)d [program:smokescreen] command=smokescreen --listen-ip=127.0.0.1 %(ENV_SMOKESCREEN_OPTS)s diff --git a/tests/cypress/e2e/actions_tasks2/case_26_canvas_color_settings_feature.js b/tests/cypress/e2e/actions_tasks2/case_26_canvas_color_settings_feature.js index 53efdb5ff494..adb2a294d561 100644 --- a/tests/cypress/e2e/actions_tasks2/case_26_canvas_color_settings_feature.js +++ b/tests/cypress/e2e/actions_tasks2/case_26_canvas_color_settings_feature.js @@ -58,6 +58,7 @@ context('Canvas color settings feature', () => { }); }); }); + cy.get('.ant-tooltip').invoke('hide'); } function checkSlidersValue(wrapper, slidersClassNames, expectedResult) { @@ -113,5 +114,26 @@ context('Canvas color settings feature', () => { '.cvat-image-setups-filters', filterSlidersClassNames, defaultValueInSettingFilters, ); }); + + it('Check persisting image filters across jobs', () => { + const sliderAction = generateString(countActionMoveSlider, 'rightarrow'); + const filterAction = generateString(countActionMoveFilterSlider, 'rightarrow'); + applyStringAction( + '.cvat-canvas-image-setups-content', classNameSliders, sliderAction, + ); + applyStringAction( + '.cvat-image-setups-filters', filterSlidersClassNames, filterAction, + ); + cy.interactMenu('Open the task'); + cy.openJob(1); + cy.get('.cvat-canvas-image-setups-trigger').click(); + checkSlidersValue( + '.cvat-canvas-image-setups-content', classNameSliders, expectedResultInSetting, + ); + checkSlidersValue( + '.cvat-image-setups-filters', filterSlidersClassNames, expectedResultInSettingFilters, + ); + cy.get('.cvat-notification-notice-image-processing-error').should('not.exist'); + }); }); }); diff --git a/tests/cypress/e2e/canvas3d_functionality/assets/test_canvas3d.zip b/tests/cypress/e2e/canvas3d_functionality/assets/test_canvas3d.zip index 3428f1da8ead..fc2b11f828f9 100644 Binary files a/tests/cypress/e2e/canvas3d_functionality/assets/test_canvas3d.zip and b/tests/cypress/e2e/canvas3d_functionality/assets/test_canvas3d.zip differ diff --git a/tests/python/README.md b/tests/python/README.md index aa80467524a7..d9a62cc71203 100644 --- a/tests/python/README.md +++ b/tests/python/README.md @@ -94,6 +94,8 @@ If you have updated the test database and want to update the assets/*.json files as well, run the appropriate script: ``` +cd tests/python +pytest ./ --start-services python shared/utils/dump_objects.py ``` diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 9d646565f3e2..29c00622d293 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -34,6 +34,7 @@ from shared.utils.config import ( BASE_URL, USER_PASS, + delete_method, get_method, make_api_client, patch_method, @@ -534,6 +535,63 @@ def test_remove_first_keyframe(self): response = patch_method("admin1", endpoint, annotations, action="update") assert response.status_code == HTTPStatus.OK + def test_can_split_skeleton_tracks_on_jobs(self, jobs): + # https://github.com/opencv/cvat/pull/6968 + task_id = 21 + + task_jobs = [job for job in jobs if job["task_id"] == task_id] + + frame_ranges = {} + for job in task_jobs: + frame_ranges[job["id"]] = set(range(job["start_frame"], job["stop_frame"] + 1)) + + # skeleton track that covers few jobs + annotations = { + "tracks": [ + { + "frame": 0, + "label_id": 58, + "shapes": [{"type": "skeleton", "frame": 0, "points": []}], + "elements": [ + { + "label_id": 59, + "frame": 0, + "shapes": [ + {"type": "points", "frame": 0, "points": [1.0, 2.0]}, + {"type": "points", "frame": 2, "points": [1.0, 2.0]}, + {"type": "points", "frame": 7, "points": [1.0, 2.0]}, + ], + }, + ], + } + ] + } + + # clear task annotations + response = delete_method("admin1", f"tasks/{task_id}/annotations") + assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" + + # create skeleton track that covers few jobs + response = patch_method( + "admin1", f"tasks/{task_id}/annotations", annotations, action="create" + ) + assert response.status_code == 200, f"Cannot update task's annotations: {response.content}" + + # check that server splitted skeleton track's elements on jobs correctly + for job_id, job_frame_range in frame_ranges.items(): + response = get_method("admin1", f"jobs/{job_id}/annotations") + assert response.status_code == 200, f"Cannot get job's annotations: {response.content}" + + job_annotations = response.json() + assert len(job_annotations["tracks"]) == 1, "Expected to see only one track" + + track = job_annotations["tracks"][0] + assert track.get("elements", []), "Expected to see track with elements" + + for element in track["elements"]: + element_frames = set(shape["frame"] for shape in element["shapes"]) + assert element_frames <= job_frame_range, "Track shapes get out of job frame range" + @pytest.mark.usefixtures("restore_db_per_class") class TestGetTaskDataset: @@ -2526,3 +2584,43 @@ def test_import_annotations(self, task_kind, annotation_kind, expect_success): task.import_annotations(self.format_name, dataset_file) assert b"Could not match item id" in capture.value.body + + def test_can_export_and_import_skeleton_tracks_in_coco_format(self): + task = self.client.tasks.retrieve(14) + dataset_file = self.tmp_dir / "some_file.zip" + format_name = "COCO Keypoints 1.0" + + original_annotations = task.get_annotations() + + task.export_dataset(format_name, dataset_file, include_images=False) + task.remove_annotations() + task.import_annotations(format_name, dataset_file) + + imported_annotations = task.get_annotations() + + # Number of shapes and tracks hasn't changed + assert len(original_annotations.shapes) == len(imported_annotations.shapes) + assert len(original_annotations.tracks) == len(imported_annotations.tracks) + + # Frames of shapes, tracks and track elements hasn't changed + assert set([s.frame for s in original_annotations.shapes]) == set( + [s.frame for s in imported_annotations.shapes] + ) + assert set([t.frame for t in original_annotations.tracks]) == set( + [t.frame for t in imported_annotations.tracks] + ) + assert set( + [ + tes.frame + for t in original_annotations.tracks + for te in t.elements + for tes in te.shapes + ] + ) == set( + [ + tes.frame + for t in imported_annotations.tracks + for te in t.elements + for tes in te.shapes + ] + ) diff --git a/utils/dataset_manifest/requirements.txt b/utils/dataset_manifest/requirements.txt index 986a4a640636..30f34ba4ca3b 100644 --- a/utils/dataset_manifest/requirements.txt +++ b/utils/dataset_manifest/requirements.txt @@ -11,7 +11,7 @@ natsort==8.0.0 # via -r utils/dataset_manifest/requirements.in numpy==1.22.4 # via opencv-python-headless -opencv-python-headless==4.5.5.62 +opencv-python-headless==4.8.1.78 # via -r utils/dataset_manifest/requirements.in pillow==10.0.1 # via -r utils/dataset_manifest/requirements.in