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 && !/^' CHANGELOG.md)"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf337acfca1e..79d3dda929a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
+
+## \[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