Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
5a3be48
Initial implementation
klakhov Jan 8, 2026
7c3860e
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 13, 2026
d36613c
Fix score visualization
klakhov Jan 13, 2026
ba89329
Add virtual votes text
klakhov Jan 13, 2026
90ffefc
Make next/prev object shortcuts global
klakhov Jan 13, 2026
ed75514
Update votes logic to be stored in object
klakhov Jan 13, 2026
393f1de
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 14, 2026
8512b9a
Display score on sidebar
klakhov Jan 14, 2026
16424a5
Add score and votes to annotation filters
klakhov Jan 14, 2026
7988880
Update review mode
klakhov Jan 14, 2026
bea545e
Clean-up comments
klakhov Jan 14, 2026
1fa189a
fix linters
klakhov Jan 14, 2026
208aba0
Fix save bug with score
klakhov Jan 14, 2026
3793157
Update schema
klakhov Jan 14, 2026
dc6d57e
Scroll into view when using shortcuts
klakhov Jan 14, 2026
6154d0f
Expand object details when object is focused
klakhov Jan 14, 2026
663b0b9
Added feature: double click on object on sidebar expands it and focuses
klakhov Jan 14, 2026
7a19846
Add changelog
klakhov Jan 14, 2026
8e20888
Remove `quorum`
klakhov Jan 14, 2026
26721b7
Update changelog
klakhov Jan 14, 2026
276cd7b
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 15, 2026
b476772
Fix server tests
klakhov Jan 15, 2026
c665f0b
Update changelog
klakhov Jan 15, 2026
8089afc
Restore quorum param as 0 for default datumaro config
klakhov Jan 15, 2026
74265ee
Tmp disable consensus tests (need to rewrite them)
klakhov Jan 15, 2026
0f7cc7e
More fixes
klakhov Jan 16, 2026
1ece322
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 17, 2026
2b8c22f
Fix: score only for labeled shapes
klakhov Jan 17, 2026
4569110
Fix black
klakhov Jan 17, 2026
61471d1
Update one more asset
klakhov Jan 17, 2026
a812cfc
Add suggested fix: transform keyframes to shapes
klakhov Jan 21, 2026
e52824b
Add 'consensus' source in skeleton elements
klakhov Jan 21, 2026
5458874
Remove 'consensus_score' attr as not used
klakhov Jan 21, 2026
cd8f540
Small updates
klakhov Jan 21, 2026
92ea3fb
Fix turple error
klakhov Jan 21, 2026
577ba9d
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 23, 2026
a5fa2fe
Preserve consensus source and always show score/votes for such objects
klakhov Jan 23, 2026
e5ffb4e
Add tooltip and margin
klakhov Jan 23, 2026
8b7f9d5
Updated changelog
klakhov Jan 23, 2026
3392e30
Add score to skeleton elements
klakhov Jan 23, 2026
c1c9a76
Update annotation filter
klakhov Jan 23, 2026
9ee7aa7
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 26, 2026
5c6c40a
Use proxy approach with lock in review mode
klakhov Jan 26, 2026
7389029
Remove readonly attr where it is always false
klakhov Jan 26, 2026
cb74fde
Update tooltip align
klakhov Jan 28, 2026
5c0d397
Make score read-only
klakhov Jan 28, 2026
7b448f4
Fix unit tests
klakhov Jan 28, 2026
a57c681
Apply minor comments
klakhov Jan 28, 2026
a6cc1ae
Fix linter
klakhov Jan 28, 2026
b38b875
Merge branch 'develop' into kl/consensus-ux
klakhov Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions changelog.d/20260114_132218_klakhov_consensus_ux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### Changed

- Consensus merge function now preserves all shapes with their scores, regardless of quorum threshold
(<https://github.com/cvat-ai/cvat/pull/10172>)

### Added

- Score visualization in UI with a virtual "Votes" attribute calculated as `score × replica_jobs`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formula can stop working if we allow job creation after merging, will need to understand how to handle it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that may be an issue. But for now, as we discussed, we’ll explicitly mention in the documentation that this value is UI-only and approximate.

(<https://github.com/cvat-ai/cvat/pull/10172>)
- A user now may navigate between different shapes with shortcuts (Tab/Shift+Tab by default) in Standard, Review modes
(<https://github.com/cvat-ai/cvat/pull/10172>)
- Review mode now supports editing objects. Users can unlock and edit individual annotations as needed
(<https://github.com/cvat-ai/cvat/pull/10172>)
- Double-clicking an object in the sidebar now centers it on the canvas and expands its details
(<https://github.com/cvat-ai/cvat/pull/10172>)

### Removed
- Consensus quorum setting has been removed. All merged annotations are now kept with their consensus scores,
allowing users to filter results based on score thresholds instead
(<https://github.com/cvat-ai/cvat/pull/10172>)
5 changes: 5 additions & 0 deletions cvat-canvas/src/scss/canvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ polyline.cvat_shape_drawing_opacity {
font-style: oblique 40deg;
}

.cvat_canvas_text_score {
fill: #FFB347;
font-style: oblique 10deg;
}

.cvat_canvas_crosshair {
stroke: red;
}
Expand Down
23 changes: 21 additions & 2 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3129,7 +3129,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}

private addText(state: any, options: { textContent?: string } = {}): SVG.Text {
private addText(state: any, options: { textContent?: string, isSkeletonElement?: boolean } = {}): SVG.Text {
const { undefinedAttrValue } = this.configuration;
const content = options.textContent || this.configuration.textContent;
const withID = content.includes('id');
Expand All @@ -3138,10 +3138,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
const withSource = content.includes('source');
const withDescriptions = content.includes('descriptions');
const withDimensions = content.includes('dimensions');

const textFontSize = this.configuration.textFontSize || 12;
const {
label, clientID, attributes, source, descriptions,
label, clientID, attributes, source, descriptions, score, votes,
} = state;
const isConsensus = source === 'consensus';
const withScore = isConsensus && !options.isSkeletonElement;
const withVotes = isConsensus && !options.isSkeletonElement;

const attrNames = Object.fromEntries(state.label.attributes.map((attr) => [attr.id, attr.name]));
if (state.shapeType === 'skeleton') {
Expand All @@ -3151,7 +3155,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
textContent: [
...(withLabel ? ['label'] : []),
...(withAttr ? ['attributes'] : []),
// Note: explicitly exclude 'score' and 'votes' for skeleton elements
].join(',') || ' ',
isSkeletonElement: true,
});
}
});
Expand Down Expand Up @@ -3193,6 +3199,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addClass('cvat_canvas_text_description');
});
}
if (withScore || withVotes) {
const parts = [];
if (withScore) {
parts.push(`Score: ${score.toFixed(2)}`);
}
if (withVotes) {
parts.push(`Votes: ${votes}`);
}
block
.tspan(parts.join(', '))
.attr({ dy: '1.25em', x: 0 })
.addClass('cvat_canvas_text_score');
}
if (withAttr) {
Object.keys(attributes).forEach((attrID: string, idx: number) => {
const values = `${attributes[attrID] === undefinedAttrValue ?
Expand Down
2 changes: 2 additions & 0 deletions cvat-core/src/annotations-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default class Collection {
dimension: DimensionType;
framesInfo: BasicInjection['framesInfo'];
jobType: JobType;
consensusReplicas?: number;
}) {
this.stopFrame = data.stopFrame;

Expand Down Expand Up @@ -102,6 +103,7 @@ export default class Collection {
nextClientID: () => ++config.globalObjectsCounter,
getMasksOnFrame: (frame: number) => (this.shapes[frame] as MaskShape[])
.filter((object) => object instanceof MaskShape),
consensusReplicas: data.consensusReplicas,
};
}

Expand Down
10 changes: 10 additions & 0 deletions cvat-core/src/annotations-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ interface ConvertedObjectData {
type: ObjectType;
shape: ShapeType;
occluded: boolean;
score: number | null;
votes: number | null;
}

export default class AnnotationsFilter {
Expand Down Expand Up @@ -118,6 +120,8 @@ export default class AnnotationsFilter {
type: state.objectType,
shape: state.shapeType,
occluded: state.occluded,
score: state.score ?? null,
votes: state.votes ?? null,
};
});

Expand Down Expand Up @@ -166,6 +170,8 @@ export default class AnnotationsFilter {
shape: shape.type,
occluded: shape.occluded,
objectID: shape.clientID ?? null,
score: shape.score ?? null,
votes: null,
};
}),
tags: collection.tags.map((tag) => {
Expand All @@ -183,6 +189,8 @@ export default class AnnotationsFilter {
shape: null,
occluded: false,
objectID: tag.clientID ?? null,
score: null,
votes: null,
};
}),
tracks: collection.tracks.map((track) => {
Expand All @@ -200,6 +208,8 @@ export default class AnnotationsFilter {
shape: track.shapes[0]?.type ?? null,
occluded: null,
objectID: track.clientID ?? null,
score: null,
votes: null,
};
}),
};
Expand Down
22 changes: 22 additions & 0 deletions cvat-core/src/annotations-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ function computeNewSource(currentSource: Source): Source {
return Source.SEMI_AUTO;
}

if (currentSource === Source.CONSENSUS) {
return Source.CONSENSUS;
}

return Source.MANUAL;
}

Expand All @@ -78,6 +82,7 @@ export interface BasicInjection {
jobType: JobType;
nextClientID: () => number;
getMasksOnFrame: (frame: number) => MaskShape[];
consensusReplicas?: number;
}

type AnnotationInjection = BasicInjection & {
Expand All @@ -104,6 +109,8 @@ class Annotation {
protected readOnlyFields: string[];
protected color: string;
protected source: Source;
public score: number;
public votes: number;
public updated: number;
public attributes: Record<number, string>;
protected groupObject: {
Expand All @@ -127,6 +134,9 @@ class Annotation {
this.readOnlyFields = injection.readOnlyFields || [];
this.color = color;
this.source = injection.jobType === JobType.GROUND_TRUTH ? Source.GT : data.source;
this.score = data.score;
this.votes = injection.consensusReplicas !== undefined ?
Math.round(this.score * injection.consensusReplicas) : 0;
this.updated = Date.now();
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
Expand Down Expand Up @@ -567,6 +577,7 @@ export class Shape extends Drawn {
label_id: this.label.id,
group: this.group,
source: this.source,
score: this.score,
};

if (this.serverID !== null) {
Expand Down Expand Up @@ -606,6 +617,8 @@ export class Shape extends Drawn {
pinned: this.pinned,
frame,
source: this.source,
score: this.score,
votes: this.votes,
__internal: this.withContext(frame),
};

Expand Down Expand Up @@ -956,6 +969,8 @@ export class Track extends Drawn {
},
frame,
source: this.source,
score: this.score,
votes: this.votes,
__internal: this.withContext(frame),
};
}
Expand Down Expand Up @@ -1489,6 +1504,8 @@ export class Tag extends Annotation {
updated: this.updated,
frame,
source: this.source,
score: this.score,
votes: this.votes,
__internal: this.withContext(frame),
};
}
Expand Down Expand Up @@ -2016,6 +2033,7 @@ export class SkeletonShape extends Shape {
label_id: this.label.id,
group: this.group,
source: this.source,
score: this.score,
};

if (this.serverID !== null) {
Expand Down Expand Up @@ -2060,6 +2078,8 @@ export class SkeletonShape extends Shape {
hidden: elements.every((el) => el.hidden),
frame,
source: this.source,
score: this.score,
votes: this.votes,
__internal: this.withContext(frame),
};
}
Expand Down Expand Up @@ -3102,6 +3122,8 @@ export class SkeletonTrack extends Track {
occluded: elements.every((el) => el.occluded),
lock: elements.every((el) => el.lock),
hidden: elements.every((el) => el.hidden),
score: this.score,
votes: this.votes,
__internal: this.withContext(frame),
};
}
Expand Down
1 change: 1 addition & 0 deletions cvat-core/src/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ async function getAnnotationsFromServer(session: Job | Task): Promise<void> {
stopFrame: session instanceof Job ? session.stopFrame : session.size - 1,
labels: session.labels,
dimension: session.dimension,
consensusReplicas: session instanceof Job ? session.consensusReplicas : undefined,
framesInfo: {
isFrameDeleted: session instanceof Job ?
(frame: number) => !!getJobFramesMetaSync(session.id).deletedFrames[frame] :
Expand Down
11 changes: 0 additions & 11 deletions cvat-core/src/consensus-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ export default class ConsensusSettings {
#id: number;
#task: number;
#iouThreshold: number;
#quorum: number;
#descriptions: Record<string, string>;

constructor(initialData: SerializedConsensusSettingsData) {
this.#id = initialData.id;
this.#task = initialData.task;
this.#iouThreshold = initialData.iou_threshold;
this.#quorum = initialData.quorum;
this.#descriptions = initialData.descriptions;
}

Expand All @@ -38,14 +36,6 @@ export default class ConsensusSettings {
this.#iouThreshold = newVal;
}

get quorum(): number {
return this.#quorum;
}

set quorum(newVal: number) {
this.#quorum = newVal;
}

get descriptions(): Record<string, string> {
const descriptions: Record<string, string> = Object.keys(this.#descriptions).reduce((acc, key) => {
const camelCaseKey = _.camelCase(key);
Expand All @@ -59,7 +49,6 @@ export default class ConsensusSettings {
public toJSON(): SerializedConsensusSettingsData {
const result: SerializedConsensusSettingsData = {
iou_threshold: this.#iouThreshold,
quorum: this.#quorum,
};

return result;
Expand Down
1 change: 1 addition & 0 deletions cvat-core/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export enum Source {
AUTO = 'auto',
FILE = 'file',
GT = 'Ground truth',
CONSENSUS = 'consensus',
}

export enum EventScope {
Expand Down
13 changes: 13 additions & 0 deletions cvat-core/src/object-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface SerializedData {
color?: string;
updated?: number;
source?: Source;
score?: number;
votes?: number;
zOrder?: number;
points?: number[];
occluded?: boolean;
Expand Down Expand Up @@ -88,6 +90,8 @@ export default class ObjectState {
next: number | null;
last: number | null;
} | null;
public readonly score: number | undefined;
public readonly votes: number | undefined;
public label: Label;
public color: string;
public hidden: boolean;
Expand Down Expand Up @@ -180,6 +184,9 @@ export default class ObjectState {
objectType: serialized.objectType,
shapeType: serialized.shapeType || null,
updateFlags,
// score and votes are only available for shapes (not tracks or tags)
score: serialized.objectType === ObjectType.SHAPE ? (serialized.score ?? 1.0) : undefined,
votes: serialized.objectType === ObjectType.SHAPE ? (serialized.votes ?? 0) : undefined,
};

Object.defineProperties(
Expand All @@ -204,6 +211,12 @@ export default class ObjectState {
isGroundTruth: {
get: () => data.source === Source.GT,
},
score: {
get: () => data.score,
},
votes: {
get: () => data.votes,
},
clientID: {
get: () => data.clientID,
},
Expand Down
2 changes: 1 addition & 1 deletion cvat-core/src/server-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,6 @@ export interface SerializedQualityReportData {
export interface SerializedConsensusSettingsData {
id?: number;
task?: number;
quorum?: number;
iou_threshold?: number;
descriptions?: Record<string, string>;
}
Expand Down Expand Up @@ -423,6 +422,7 @@ export interface SerializedShape {
group: number;
frame: number;
source: Source;
score?: number;
attributes: { spec_id: number; value: string }[];
elements: Omit<SerializedShape, 'elements'>[];
occluded: boolean;
Expand Down
Loading
Loading