-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[GSoC'24] Consensus annotation #8434
base: develop
Are you sure you want to change the base?
Changes from 250 commits
f5cf75a
ea1f3cd
283578f
bf8fa7d
2da336f
df460ce
b5b6e7f
6f68b83
fc5201b
dc12a1f
a5512e7
a81f057
2a07da7
d8955f2
32190e2
b9a8b19
10e1053
e467eb3
8508e2e
c24a82d
fe3f524
d53b3ae
1c5625d
fd434ae
3b90c8f
a5224f1
27297f4
e33f9b2
2a26581
f754bc6
9866dbd
df8bd0c
4bf0aeb
033b6a8
6d49bf5
2c906bb
7635f34
a6a9def
1b206a3
1e60737
6c24859
0412835
f5a4880
89b86df
2c2ca6a
9315d67
7ede7cb
cc036a4
c186d67
93d0758
f7c20a0
d922b8f
ca4d4f2
c240ee7
0b428f8
26a7157
2af30ba
13e9edb
8170c8a
3eb8ec6
924ad45
91caee6
a487dd0
59f2daa
79c2a7b
008d14b
e6c7f58
d9466bf
7c0aefc
54e7d97
fdfc3cb
de1e7eb
06bea50
5e0bc21
11258ec
bb7f242
40585f4
4228ad2
ced9e5c
9d1fd92
d2a6789
01ef325
ca61c51
4642244
b45d795
6e8e252
755465e
7f229ad
fc5b670
1acb383
4532b28
cd05f0f
3ea3981
0bac45b
76f2d2c
d3c3750
188f9f0
3ce1295
2fb5941
145a9d2
2ac3abc
a1b2651
99e78d2
b8566a1
8fab2b4
0654857
eebc206
30c80bd
07b9c0b
8301ad2
0550298
d59568a
b079593
5db8cb0
8539683
ff4140f
8e0d4ac
6fda20f
7787fe2
0376d31
2a2efe9
b6e21ff
3f8e171
893091a
1c427ed
6bce0ac
0ceed73
2336773
9c0a545
aa3dcda
d0659b6
1ae7309
c48223e
11ef270
5530bb4
1dc43e4
05bd621
9b67b7f
26e9ab6
ddff303
6bccbbc
c0af3ee
bd86446
8c09dcf
060e518
0692c6b
ec58672
03a3c31
e4cc879
789cb74
25e247e
ecb8367
bc3a4d5
fa42b98
4767f74
373ec86
4b4847a
f92c0a2
039c076
47733ac
9353906
d3f1bab
a2c7fe7
d06d124
e544e79
bc6cd9a
2f49390
a9eb879
de91e88
8db1d21
f666c57
145fd7a
3d68358
46a5003
ba1d875
b68f761
3b158b0
795daad
81763b9
671829d
c8b6e28
0ab57db
11c9798
4a0d988
1965c72
e5910e9
d9f72fd
7e00278
15e2501
6c857b9
91a120c
b4ac890
73aa75b
1f12625
3883993
7d1965a
d28826d
053a7dd
42fed6b
85311d6
eb9c6e4
b0a762d
32c5c6a
211425e
864791d
4186fe2
a107c2c
6ae0ed7
8f8091f
0f758bb
488ebbd
909b08c
50007a2
8916612
38b9691
86eeb0e
da956e5
5e89857
0c17d44
33e4b9d
939cb10
bff16ba
da1aa0d
051992e
27117b6
786c135
104b2bb
efa9c53
8441428
1c0c88d
befeb37
4674adc
c7cbedb
505318a
9afad1a
b957929
e6eccf2
7551811
8efe8fa
966b6e5
f86a394
af04e2b
9b4ca32
dac855f
06995ee
93c1dcb
e2d96af
24d79a3
8dc3c2c
fc212f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
### Changed | ||
|
||
- Quality analytics page will now report job assignees from quality reports | ||
instead of current job assignees | ||
(<https://github.com/cvat-ai/cvat/pull/8123>) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
### Added | ||
|
||
- Datumaro format now supports skeletons | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect merge Please, add correct changelog entry, describing what was implemented in this patch |
||
(<https://github.com/cvat-ai/cvat/pull/8165>) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,8 +31,11 @@ import Organization, { Invitation } from './organization'; | |
import Webhook from './webhook'; | ||
import { ArgumentError } from './exceptions'; | ||
import { | ||
AnalyticsReportFilter, QualityConflictsFilter, QualityReportsFilter, | ||
AnalyticsReportFilter, ConflictsFilter, QualityReportsFilter, | ||
QualitySettingsFilter, SerializedAsset, | ||
ConsensusReportsFilter, | ||
AssigneeConsensusReportsFilter, | ||
ConsensusSettingsFilter, | ||
} from './server-response-types'; | ||
import QualityReport from './quality-report'; | ||
import QualityConflict, { ConflictSeverity } from './quality-conflict'; | ||
|
@@ -44,11 +47,85 @@ import { convertDescriptions, getServerAPISchema } from './server-schema'; | |
import { JobType } from './enums'; | ||
import { PaginatedResource } from './core-types'; | ||
import CVATCore from '.'; | ||
import ConsensusSettings from './consensus-settings'; | ||
import ConsensusReport from './consensus-report'; | ||
import AssigneeConsensusReport from './assignee-consensus-report'; | ||
import ConsensusConflict from './consensus-conflict'; | ||
|
||
function implementationMixin(func: Function, implementation: Function): void { | ||
Object.assign(func, { implementation }); | ||
} | ||
|
||
type ConflictType = ConsensusConflict | QualityConflict; | ||
|
||
function mergeConflicts<T extends ConflictType>(conflicts: T[]): T[] { | ||
const frames = Array.from(new Set(conflicts.map((conflict) => conflict.frame))) | ||
.sort((a, b) => a - b); | ||
|
||
const mergedConflicts: T[] = []; | ||
|
||
for (const frame of frames) { | ||
const frameConflicts = conflicts.filter((conflict) => conflict.frame === frame); | ||
const conflictsByObject: Record<string, T[]> = {}; | ||
|
||
frameConflicts.forEach((qualityConflict: T) => { | ||
const { type, serverID } = qualityConflict.annotationConflicts[0]; | ||
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any reason to remove comments that was left in original function? If not, please return them |
||
const firstObjID = `${type}_${serverID}`; | ||
conflictsByObject[firstObjID] = conflictsByObject[firstObjID] || []; | ||
conflictsByObject[firstObjID].push(qualityConflict); | ||
}); | ||
|
||
for (const objectConflicts of Object.values(conflictsByObject)) { | ||
if (objectConflicts.length === 1) { | ||
mergedConflicts.push(objectConflicts[0]); | ||
} else { | ||
const [firstConflict] = objectConflicts; | ||
let mainObjectConflict: T; | ||
|
||
if (firstConflict instanceof QualityConflict) { | ||
mainObjectConflict = objectConflicts.find( | ||
(conflict) => (conflict as QualityConflict).severity === ConflictSeverity.ERROR, | ||
) || firstConflict; | ||
} else { | ||
mainObjectConflict = firstConflict; | ||
} | ||
const descriptionList: string[] = [mainObjectConflict.description]; | ||
|
||
for (const objectConflict of objectConflicts) { | ||
if (objectConflict !== mainObjectConflict) { | ||
descriptionList.push(objectConflict.description); | ||
|
||
for (const annotationConflict of objectConflict.annotationConflicts) { | ||
if (!mainObjectConflict.annotationConflicts.find((_annotationConflict) => ( | ||
_annotationConflict.serverID === annotationConflict.serverID && | ||
_annotationConflict.type === annotationConflict.type)) | ||
) { | ||
mainObjectConflict.annotationConflicts.push(annotationConflict); | ||
} | ||
} | ||
} | ||
} | ||
|
||
const description = descriptionList.join(', '); | ||
const visibleConflict = new Proxy(mainObjectConflict, { | ||
get(target, prop) { | ||
if (prop === 'description') { | ||
return description; | ||
} | ||
|
||
const val = Reflect.get(target, prop); | ||
return typeof val === 'function' ? (...args: any[]) => val.apply(target, args) : val; | ||
}, | ||
}); | ||
|
||
mergedConflicts.push(visibleConflict); | ||
} | ||
} | ||
} | ||
|
||
return mergedConflicts; | ||
} | ||
|
||
export default function implementAPI(cvat: CVATCore): CVATCore { | ||
implementationMixin(cvat.plugins.list, PluginRegistry.list); | ||
implementationMixin(cvat.plugins.register, PluginRegistry.register.bind(cvat)); | ||
|
@@ -434,7 +511,7 @@ export default function implementAPI(cvat: CVATCore): CVATCore { | |
); | ||
return reports; | ||
}); | ||
implementationMixin(cvat.analytics.quality.conflicts, async (filter: QualityConflictsFilter) => { | ||
implementationMixin(cvat.analytics.quality.conflicts, async (filter: ConflictsFilter) => { | ||
checkFilter(filter, { | ||
reportID: isInteger, | ||
}); | ||
|
@@ -443,72 +520,7 @@ export default function implementAPI(cvat: CVATCore): CVATCore { | |
|
||
const conflictsData = await serverProxy.analytics.quality.conflicts(params); | ||
const conflicts = conflictsData.map((conflict) => new QualityConflict({ ...conflict })); | ||
const frames = Array.from(new Set(conflicts.map((conflict) => conflict.frame))) | ||
.sort((a, b) => a - b); | ||
|
||
// each QualityConflict may have several AnnotationConflicts bound | ||
// at the same time, many quality conflicts may refer | ||
// to the same labeled object (e.g. mismatch label, low overlap) | ||
// the code below unites quality conflicts bound to the same object into one QualityConflict object | ||
const mergedConflicts: QualityConflict[] = []; | ||
|
||
for (const frame of frames) { | ||
const frameConflicts = conflicts.filter((conflict) => conflict.frame === frame); | ||
const conflictsByObject: Record<string, QualityConflict[]> = {}; | ||
|
||
frameConflicts.forEach((qualityConflict: QualityConflict) => { | ||
const { type, serverID } = qualityConflict.annotationConflicts[0]; | ||
const firstObjID = `${type}_${serverID}`; | ||
conflictsByObject[firstObjID] = conflictsByObject[firstObjID] || []; | ||
conflictsByObject[firstObjID].push(qualityConflict); | ||
}); | ||
|
||
for (const objectConflicts of Object.values(conflictsByObject)) { | ||
if (objectConflicts.length === 1) { | ||
// only one quality conflict refers to the object on current frame | ||
mergedConflicts.push(objectConflicts[0]); | ||
} else { | ||
const mainObjectConflict = objectConflicts | ||
.find((conflict) => conflict.severity === ConflictSeverity.ERROR) || objectConflicts[0]; | ||
const descriptionList: string[] = [mainObjectConflict.description]; | ||
|
||
for (const objectConflict of objectConflicts) { | ||
if (objectConflict !== mainObjectConflict) { | ||
descriptionList.push(objectConflict.description); | ||
|
||
for (const annotationConflict of objectConflict.annotationConflicts) { | ||
if (!mainObjectConflict.annotationConflicts.find((_annotationConflict) => ( | ||
_annotationConflict.serverID === annotationConflict.serverID && | ||
_annotationConflict.type === annotationConflict.type)) | ||
) { | ||
mainObjectConflict.annotationConflicts.push(annotationConflict); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// decorate the original conflict to avoid changing it | ||
const description = descriptionList.join(', '); | ||
const visibleConflict = new Proxy(mainObjectConflict, { | ||
get(target, prop) { | ||
if (prop === 'description') { | ||
return description; | ||
} | ||
|
||
// By default, it looks like Reflect.get(target, prop, receiver) | ||
// which has a different value of `this`. It doesn't allow to | ||
// work with methods / properties that use private members. | ||
const val = Reflect.get(target, prop); | ||
return typeof val === 'function' ? (...args: any[]) => val.apply(target, args) : val; | ||
}, | ||
}); | ||
|
||
mergedConflicts.push(visibleConflict); | ||
} | ||
} | ||
} | ||
|
||
return mergedConflicts; | ||
return mergeConflicts(conflicts); | ||
}); | ||
implementationMixin(cvat.analytics.quality.settings.get, async (filter: QualitySettingsFilter) => { | ||
checkFilter(filter, { | ||
|
@@ -523,6 +535,58 @@ export default function implementAPI(cvat: CVATCore): CVATCore { | |
|
||
return new QualitySettings({ ...settings, descriptions }); | ||
}); | ||
implementationMixin(cvat.consensus.reports, async (filter: ConsensusReportsFilter) => { | ||
checkFilter(filter, { | ||
page: isInteger, | ||
pageSize: isPageSize, | ||
projectID: isInteger, | ||
taskID: isInteger, | ||
jobID: isInteger, | ||
filter: isString, | ||
search: isString, | ||
target: isString, | ||
sort: isString, | ||
}); | ||
|
||
const params = fieldsToSnakeCase({ ...filter, sort: '-created_date' }); | ||
|
||
const reportsData = await serverProxy.consensus.reports(params); | ||
const reports = Object.assign( | ||
reportsData.map((report) => new ConsensusReport({ ...report })), | ||
{ count: reportsData.count }, | ||
); | ||
return reports; | ||
}); | ||
implementationMixin(cvat.consensus.assigneeReports, async (filter: AssigneeConsensusReportsFilter) => { | ||
checkFilter(filter, { | ||
page: isInteger, | ||
pageSize: isPageSize, | ||
taskID: isInteger, | ||
filter: isString, | ||
consensusReportID: isInteger, | ||
search: isString, | ||
sort: isString, | ||
}); | ||
|
||
const params = fieldsToSnakeCase({ ...filter, sort: '-id' }); | ||
|
||
const reportsData = await serverProxy.consensus.assignee_reports(params); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, use |
||
const reports = Object.assign( | ||
reportsData.map((report) => new AssigneeConsensusReport({ ...report })), | ||
{ count: reportsData.count }, | ||
); | ||
return reports; | ||
}); | ||
implementationMixin(cvat.consensus.settings.get, async (filter: ConsensusSettingsFilter) => { | ||
checkFilter(filter, { | ||
taskID: isInteger, | ||
}); | ||
|
||
const params = fieldsToSnakeCase(filter); | ||
|
||
const settings = await serverProxy.consensus.settings.get(params); | ||
return new ConsensusSettings({ ...settings }); | ||
}); | ||
implementationMixin(cvat.analytics.performance.reports, async (filter: AnalyticsReportFilter) => { | ||
checkFilter(filter, { | ||
jobID: isInteger, | ||
|
@@ -538,6 +602,17 @@ export default function implementAPI(cvat: CVATCore): CVATCore { | |
const reportData = await serverProxy.analytics.performance.reports(params); | ||
return new AnalyticsReport(reportData); | ||
}); | ||
implementationMixin(cvat.consensus.conflicts, async (filter: ConflictsFilter) => { | ||
checkFilter(filter, { | ||
reportID: isInteger, | ||
}); | ||
|
||
const params = fieldsToSnakeCase(filter); | ||
|
||
const conflictsData = await serverProxy.consensus.conflicts(params); | ||
const conflicts = conflictsData.map((conflict) => new ConsensusConflict({ ...conflict })); | ||
return mergeConflicts(conflicts); | ||
}); | ||
implementationMixin(cvat.analytics.performance.calculate, async ( | ||
body: Parameters<CVATCore['analytics']['performance']['calculate']>[0], | ||
onUpdate: Parameters<CVATCore['analytics']['performance']['calculate']>[1], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Copyright (C) 2024 CVAT.ai Corporation | ||
// | ||
// SPDX-License-Identifier: MIT | ||
|
||
import { SerializedAssigneeConsensusReportData } from './server-response-types'; | ||
import User from './user'; | ||
|
||
export default class AssigneeConsensusReport { | ||
#id: number; | ||
#taskID: number; | ||
#assignee: User; | ||
#consensusScore: number; | ||
#conflictCount: number; | ||
#consensusReportID: number; | ||
|
||
constructor(initialData: SerializedAssigneeConsensusReportData) { | ||
this.#id = initialData.id; | ||
this.#taskID = initialData.task_id; | ||
this.#consensusScore = initialData.consensus_score; | ||
this.#consensusReportID = initialData.consensus_report_id; | ||
this.#conflictCount = initialData.conflict_count; | ||
|
||
if (initialData.assignee) { | ||
this.#assignee = new User(initialData.assignee); | ||
} else { | ||
this.#assignee = null; | ||
} | ||
} | ||
|
||
get id(): number { | ||
return this.#id; | ||
} | ||
|
||
get taskID(): number { | ||
return this.#taskID; | ||
} | ||
|
||
get assignee(): User { | ||
return this.#assignee; | ||
} | ||
|
||
get consensusScore(): number { | ||
return this.#consensusScore; | ||
} | ||
|
||
get conflictCount(): number { | ||
return this.#conflictCount; | ||
} | ||
|
||
get consensusReportID(): number { | ||
return this.#consensusReportID; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect merge