Skip to content

Commit 4d06ae1

Browse files
Consensus simple merging (#8953)
- Added support for consensus task and consensus job merging (API and UI) - Added simple consensus settings - Added server tests - Added new `consensus` RQ queue and worker - Updated skeleton comparisons: hidden points now also contribute to the skeleton similarity. Only visibility is taken into account for invisible points Limitations: - Merging is supported for all annotations except 2d and 3d cuboids. 3d tasks are not supported - Annotation groups are not supported (each annotation is considered separate in a group) - Polygons and masks are not interchangeable (each type is compared only with the same type) Co-authored-by: Kirill Lakhov <[email protected]>
1 parent c25bf96 commit 4d06ae1

File tree

81 files changed

+10137
-1014
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+10137
-1014
lines changed

.vscode/launch.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,26 @@
125125
],
126126
"justMyCode": false,
127127
},
128+
{
129+
"name": "REST API tests: Attach to RQ consensus worker",
130+
"type": "debugpy",
131+
"request": "attach",
132+
"connect": {
133+
"host": "127.0.0.1",
134+
"port": 9096
135+
},
136+
"pathMappings": [
137+
{
138+
"localRoot": "${workspaceFolder}",
139+
"remoteRoot": "/home/django/"
140+
},
141+
{
142+
"localRoot": "${workspaceFolder}/.env",
143+
"remoteRoot": "/opt/venv",
144+
}
145+
],
146+
"justMyCode": false,
147+
},
128148
{
129149
"type": "pwa-chrome",
130150
"request": "launch",
@@ -383,6 +403,28 @@
383403
},
384404
"console": "internalConsole"
385405
},
406+
{
407+
"name": "server: RQ - consensus",
408+
"type": "debugpy",
409+
"request": "launch",
410+
"stopOnEntry": false,
411+
"justMyCode": false,
412+
"python": "${command:python.interpreterPath}",
413+
"program": "${workspaceRoot}/manage.py",
414+
"args": [
415+
"rqworker",
416+
"consensus",
417+
"--worker-class",
418+
"cvat.rqworker.SimpleWorker"
419+
],
420+
"django": true,
421+
"cwd": "${workspaceFolder}",
422+
"env": {
423+
"DJANGO_LOG_SERVER_HOST": "localhost",
424+
"DJANGO_LOG_SERVER_PORT": "8282"
425+
},
426+
"console": "internalConsole"
427+
},
386428
{
387429
"name": "server: migrate",
388430
"type": "debugpy",
@@ -566,6 +608,7 @@
566608
"server: RQ - analytics reports",
567609
"server: RQ - cleaning",
568610
"server: RQ - chunks",
611+
"server: RQ - consensus",
569612
]
570613
}
571614
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
### Added
2+
3+
- Simple merging for consensus-enabled tasks
4+
(<https://github.com/cvat-ai/cvat/pull/8953>)
5+
6+
### Changed
7+
8+
- Hidden points in skeletons now also contribute to the skeleton similarity
9+
in quality computations and in consensus merging
10+
(<https://github.com/cvat-ai/cvat/pull/8953>)

cvat-core/src/api-implementation.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ import Webhook from './webhook';
3232
import { ArgumentError } from './exceptions';
3333
import {
3434
AnalyticsReportFilter, QualityConflictsFilter, QualityReportsFilter,
35-
QualitySettingsFilter, SerializedAsset,
35+
QualitySettingsFilter, SerializedAsset, ConsensusSettingsFilter,
3636
} from './server-response-types';
3737
import QualityReport from './quality-report';
3838
import AboutData from './about';
3939
import QualityConflict, { ConflictSeverity } from './quality-conflict';
4040
import QualitySettings from './quality-settings';
4141
import { getFramesMeta } from './frames';
4242
import AnalyticsReport from './analytics-report';
43+
import ConsensusSettings from './consensus-settings';
4344
import {
4445
callAction, listActions, registerAction, runAction,
4546
} from './annotations-actions/annotations-actions';
@@ -415,6 +416,20 @@ export default function implementAPI(cvat: CVATCore): CVATCore {
415416
return webhooks;
416417
});
417418

419+
implementationMixin(cvat.consensus.settings.get, async (filter: ConsensusSettingsFilter) => {
420+
checkFilter(filter, {
421+
taskID: isInteger,
422+
});
423+
424+
const params = fieldsToSnakeCase(filter);
425+
426+
const settings = await serverProxy.consensus.settings.get(params);
427+
const schema = await getServerAPISchema();
428+
const descriptions = convertDescriptions(schema.components.schemas.ConsensusSettings.properties);
429+
430+
return new ConsensusSettings({ ...settings, descriptions });
431+
});
432+
418433
implementationMixin(cvat.analytics.quality.reports, async (filter: QualityReportsFilter) => {
419434
checkFilter(filter, {
420435
page: isInteger,

cvat-core/src/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,14 @@ function build(): CVATCore {
378378
return result;
379379
},
380380
},
381+
consensus: {
382+
settings: {
383+
async get(filter = {}) {
384+
const result = await PluginRegistry.apiWrapper(cvat.consensus.settings.get, filter);
385+
return result;
386+
},
387+
},
388+
},
381389
analytics: {
382390
performance: {
383391
async reports(filter = {}) {
@@ -482,6 +490,7 @@ function build(): CVATCore {
482490
cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
483491
cvat.organizations = Object.freeze(cvat.organizations);
484492
cvat.webhooks = Object.freeze(cvat.webhooks);
493+
cvat.consensus = Object.freeze(cvat.consensus);
485494
cvat.analytics = Object.freeze(cvat.analytics);
486495
cvat.classes = Object.freeze(cvat.classes);
487496
cvat.utils = Object.freeze(cvat.utils);

cvat-core/src/consensus-settings.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (C) 2024 CVAT.ai Corporation
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
import { SerializedConsensusSettingsData } from './server-response-types';
6+
import PluginRegistry from './plugins';
7+
import serverProxy from './server-proxy';
8+
import { convertDescriptions, getServerAPISchema } from './server-schema';
9+
10+
export default class ConsensusSettings {
11+
#id: number;
12+
#task: number;
13+
#iouThreshold: number;
14+
#quorum: number;
15+
#descriptions: Record<string, string>;
16+
17+
constructor(initialData: SerializedConsensusSettingsData) {
18+
this.#id = initialData.id;
19+
this.#task = initialData.task;
20+
this.#iouThreshold = initialData.iou_threshold;
21+
this.#quorum = initialData.quorum;
22+
this.#descriptions = initialData.descriptions;
23+
}
24+
25+
get id(): number {
26+
return this.#id;
27+
}
28+
29+
get task(): number {
30+
return this.#task;
31+
}
32+
33+
get iouThreshold(): number {
34+
return this.#iouThreshold;
35+
}
36+
37+
set iouThreshold(newVal: number) {
38+
this.#iouThreshold = newVal;
39+
}
40+
41+
get quorum(): number {
42+
return this.#quorum;
43+
}
44+
45+
set quorum(newVal: number) {
46+
this.#quorum = newVal;
47+
}
48+
49+
get descriptions(): Record<string, string> {
50+
const descriptions: Record<string, string> = Object.keys(this.#descriptions).reduce((acc, key) => {
51+
const camelCaseKey = _.camelCase(key);
52+
acc[camelCaseKey] = this.#descriptions[key];
53+
return acc;
54+
}, {});
55+
56+
return descriptions;
57+
}
58+
59+
public toJSON(): SerializedConsensusSettingsData {
60+
const result: SerializedConsensusSettingsData = {
61+
iou_threshold: this.#iouThreshold,
62+
quorum: this.#quorum,
63+
};
64+
65+
return result;
66+
}
67+
68+
public async save(): Promise<ConsensusSettings> {
69+
const result = await PluginRegistry.apiWrapper.call(this, ConsensusSettings.prototype.save);
70+
return result;
71+
}
72+
}
73+
74+
Object.defineProperties(ConsensusSettings.prototype.save, {
75+
implementation: {
76+
writable: false,
77+
enumerable: false,
78+
value: async function implementation(): Promise<ConsensusSettings> {
79+
const result = await serverProxy.consensus.settings.update(
80+
this.id, this.toJSON(),
81+
);
82+
const schema = await getServerAPISchema();
83+
const descriptions = convertDescriptions(schema.components.schemas.ConsensusSettings.properties);
84+
return new ConsensusSettings({ ...result, descriptions });
85+
},
86+
},
87+
});

cvat-core/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import {
66
AnalyticsReportFilter, QualityConflictsFilter, QualityReportsFilter, QualitySettingsFilter,
7+
ConsensusSettingsFilter,
78
} from './server-response-types';
89
import PluginRegistry from './plugins';
910
import serverProxy from './server-proxy';
@@ -30,6 +31,7 @@ import Webhook from './webhook';
3031
import QualityReport from './quality-report';
3132
import QualityConflict from './quality-conflict';
3233
import QualitySettings from './quality-settings';
34+
import ConsensusSettings from './consensus-settings';
3335
import AnalyticsReport from './analytics-report';
3436
import AnnotationGuide from './guide';
3537
import { JobValidationLayout, TaskValidationLayout } from './validation-layout';
@@ -140,6 +142,11 @@ export default interface CVATCore {
140142
webhooks: {
141143
get: any;
142144
};
145+
consensus: {
146+
settings: {
147+
get: (filter: ConsensusSettingsFilter) => Promise<ConsensusSettings>;
148+
};
149+
}
143150
analytics: {
144151
quality: {
145152
reports: (filter: QualityReportsFilter) => Promise<PaginatedResource<QualityReport>>;

cvat-core/src/server-proxy.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
SerializedInvitationData, SerializedCloudStorage, SerializedFramesMetaData, SerializedCollection,
2020
SerializedQualitySettingsData, APIQualitySettingsFilter, SerializedQualityConflictData, APIQualityConflictsFilter,
2121
SerializedQualityReportData, APIQualityReportsFilter, SerializedAnalyticsReport, APIAnalyticsReportFilter,
22+
SerializedConsensusSettingsData, APIConsensusSettingsFilter,
2223
SerializedRequest, SerializedJobValidationLayout, SerializedTaskValidationLayout,
2324
} from './server-response-types';
2425
import { PaginatedResource, UpdateStatusData } from './core-types';
@@ -767,6 +768,41 @@ async function deleteTask(id: number, organizationID: string | null = null): Pro
767768
}
768769
}
769770

771+
async function mergeConsensusJobs(id: number, instanceType: string): Promise<void> {
772+
const { backendAPI } = config;
773+
const url = `${backendAPI}/consensus/merges`;
774+
const params = {
775+
rq_id: null,
776+
};
777+
const requestBody = {
778+
task_id: undefined,
779+
job_id: undefined,
780+
};
781+
782+
if (instanceType === 'task') requestBody.task_id = id;
783+
else requestBody.job_id = id;
784+
785+
return new Promise<void>((resolve, reject) => {
786+
async function request() {
787+
try {
788+
const response = await Axios.post(url, requestBody, { params });
789+
params.rq_id = response.data.rq_id;
790+
const { status } = response;
791+
if (status === 202) {
792+
setTimeout(request, 3000);
793+
} else if (status === 201) {
794+
resolve();
795+
} else {
796+
reject(generateError(response));
797+
}
798+
} catch (errorData) {
799+
reject(generateError(errorData));
800+
}
801+
}
802+
setTimeout(request);
803+
});
804+
}
805+
770806
async function getLabels(filter: {
771807
job_id?: number,
772808
task_id?: number,
@@ -2182,6 +2218,42 @@ async function updateQualitySettings(
21822218
}
21832219
}
21842220

2221+
async function getConsensusSettings(
2222+
filter: APIConsensusSettingsFilter,
2223+
): Promise<SerializedConsensusSettingsData> {
2224+
const { backendAPI } = config;
2225+
2226+
try {
2227+
const response = await Axios.get(`${backendAPI}/consensus/settings`, {
2228+
params: {
2229+
...filter,
2230+
},
2231+
});
2232+
2233+
return response.data.results[0];
2234+
} catch (errorData) {
2235+
throw generateError(errorData);
2236+
}
2237+
}
2238+
2239+
async function updateConsensusSettings(
2240+
settingsID: number,
2241+
settingsData: SerializedConsensusSettingsData,
2242+
): Promise<SerializedConsensusSettingsData> {
2243+
const params = enableOrganization();
2244+
const { backendAPI } = config;
2245+
2246+
try {
2247+
const response = await Axios.patch(`${backendAPI}/consensus/settings/${settingsID}`, settingsData, {
2248+
params,
2249+
});
2250+
2251+
return response.data;
2252+
} catch (errorData) {
2253+
throw generateError(errorData);
2254+
}
2255+
}
2256+
21852257
async function getQualityConflicts(
21862258
filter: APIQualityConflictsFilter,
21872259
): Promise<SerializedQualityConflictData[]> {
@@ -2411,6 +2483,7 @@ export default Object.freeze({
24112483
backup: backupTask,
24122484
restore: restoreTask,
24132485
validationLayout: validationLayout('tasks'),
2486+
mergeConsensusJobs,
24142487
}),
24152488

24162489
labels: Object.freeze({
@@ -2427,6 +2500,7 @@ export default Object.freeze({
24272500
delete: deleteJob,
24282501
exportDataset: exportDataset('jobs'),
24292502
validationLayout: validationLayout('jobs'),
2503+
mergeConsensusJobs,
24302504
}),
24312505

24322506
users: Object.freeze({
@@ -2531,6 +2605,13 @@ export default Object.freeze({
25312605
}),
25322606
}),
25332607

2608+
consensus: Object.freeze({
2609+
settings: Object.freeze({
2610+
get: getConsensusSettings,
2611+
update: updateConsensusSettings,
2612+
}),
2613+
}),
2614+
25342615
requests: Object.freeze({
25352616
list: getRequestsList,
25362617
status: getRequestStatus,

0 commit comments

Comments
 (0)